A unit test is an automated code-level test for a small “unit” of functionality. Unit tests are often designed to test a broad range of the expected functionality, including any weird corner cases and some tests that should not work. They tend to interact minimally with external resources like the disk, the network, and databases; testing code that accesses these resources is usually put under functional tests, regression tests, or integration tests.
(There’s lots of discussion on whether unit tests should do things like access external resources, and whether or not they are still “unit” tests if they do. The arguments are fun to read, and I encourage you to read them. I’m going to stick with a fairly pragmatic and broad definition: anything that exercises a small, fairly isolated piece of functionality is a unit test.)
Unit tests are almost always pretty simple, by intent; for example, if you wanted to test an (intentionally naive) regular expression for validating the form of e-mail addresses, your test might look something like this:
EMAIL_REGEXP = r'[\S.]+@[\S.]+'
def test_email_regexp():
# a regular e-mail address should match
assert re.match(EMAIL_REGEXP, 'test@nowhere.com')
# no domain should fail
assert not re.match(EMAIL_REGEXP, 'test@')
There are a couple of ways to integrate unit tests into your development style. These include Test Driven Development, where unit tests are written prior to the functionality they’re testing; during refactoring, where existing code – sometimes code without any automated tests to start with – is retrofitted with unit tests as part of the refactoring process; bug fix testing, where bugs are first pinpointed by a targetted test and then fixed; and straight test enhanced development, where tests are written organically as the code evolves. In the end, I think it matters more that you’re writing unit tests than it does exactly how you write them.
For me, the most important part of having unit tests is that they can be run quickly, easily, and without any thought by developers. They serve as executable, enforceable documentation for function and API, and they also serve as an invaluable reminder of bugs you’ve fixed in the past. As such, they improve my ability to more quickly deliver functional code – and that’s really the bottom line.