I've been asked to give an impromptu talk about unit-testing to another team. I looked over our test codebase for inspiration as to what points I should cover...
Distinguish between integration testing and unit testing. It
is key that units can be isolated. Interfaces are the tool to
achieve this. Good unit testing can reduce the need for
integration testing.
Unit testing: automated vs. manual, every build vs.
infrequently (e.g. load-testing, performance-testing,
torture-tests)
Use logging to manually review complex processes (e.g.
pooling, threading, resource acquisition and release,
synchronization)
Even in mature systems, write unit tests for small pieces as
you go in to touch them. Don't be discouraged just because you
can't get full coverage.
Instrument user interfaces to record and playback test cases.
It can then be much simpler to write test cases.
Create simple (unpublished) interactive interfaces to complex
components so that tests can be made up and run on the spot. Such
a facility often suggests new automated test cases. And there is
nothing like human eyes to spot errors that even carefully design
test suites fail to anticipate.
Try to keep tests self-contained. Test database components
against local databases (e.g. Access files, In-memory databases,
etc). It is frustrating when the nightly build fails because a
unit test fails when some externally managed server goes down --
or the data changes.
Use non-deterministic tests to test performance requirements,
or to detect memory leaks, dangling pointers, etc. In the latter
case, running the test suite itself a few hundred times (minus
the leak test, of course) provides an excellent test
environment.
If you must use external databases, try to generate the test
data with each test run.
Use all of your normal programming ingenuity to write
comprehensive test cases. Create supporting test frameworks. Use
O-O techniques to capture commonality between test cases. Use
data structures to, for example, compare two randomly sorted
result sets for equality. Assemble a library of useful test
support routines, within projects and across projects. Use good
algorithms to generate credible test data and to verify the
results. Dispose of temporary resources, especially persistent
ones like temporary files or database tables.
Document test suites so that they read like detailed
specifications.
Use the same test cases against all implementers of an
interface. This may necessitate mandating that implementers must
have associated test code that will produce and consume standard
data sets. The interface test code should first verify that the
implementing component meets this criterion before running the
rest of the tests.
Leave hooks in the real code that test code can exploit.
Strive to keep the hooks private to the component, especially in
components that have security roles.
Use a development environment that allows test cases to be run
piecemeal, preferably avoiding a full compile-link-load-run
cycle, and avoids running the whole test suite (or system) just
to run one test. A tight cycle reduces the temptation to skip
running (or writing) tests.
If your environment does not have a testing framework, write
one. They are not too hard, and you can grow it
incrementally.
Stay focussed in your testing. Just because a component is
usually invoked within a certain environment, does not mean that
a test has to take place there. All you have to do is simulate
the environment (but make sure your simulation is accurate!).
Test cases grow exponentially with component complexity, so keep
components small. Design for isolated testability. Good unit
tests for the containing environment will test the
integration.
Try to write good black box tests, that have complete
coverage, but do not neglect white box tests. Some possible test
scenarios are not identifiable from outside a component, such as
strategy changes.
Make tests repeatable. If generating random data, make sure
that the seed is either hard-coded or at least reported so that a
failure can be reproduced as necessary. If creating simulated
load or concurrency conditions, use positive measures to ensure
repeatability (such as explicitly synchronizing the events in two
threads instead of leaving their coincidence to chance, or
relying on certain performance characteristics [e.g. clock speed]
of the test machine).
Avoid enshrining accidental features of an interface in the
test code. For example, if an interface returns a list of items
with no ordering guarantee, don't rely on the fact that the
current implementation happens to return them in a certain
order.
Use UI robot technologies to test UIs. Don't be afraid to
write code that injects simulated events to your UIs -- in many
frameworks this is not terribly hard to do.
Verify that your tests fail when erroneous behaviour occurs.
If you write your tests before you write the code that implements
the tested functionality, you get this check for free -- until
the implementation occurs. After that, this situation becomes
more difficult to verify. There exist utilities that provide
automation for this task, 'breaking' unit tests by changing
subtle features in the tests like expected literal strings and so
on, but these automated methods only scratch the surface. This is
an outstanding unsolved problem. Occasionally break a test on
purpose, just to make sure things are acting as expected.
Use instrumented implementers of interfaces with 'dispose()'
and 'close()' methods to verify that infrastructures that use
them do, in fact, dispose of the objects exactly once.
Test source code is *real* source code. Keep it in the project
tree. Keep it in the version control system.
Parameterize variable and sensitive test configuration items,
such as database server names, user ids, passwords, etc, and make
it possible to configure these items externally when the test is
run. (e.g. command line arguments, environment variables, INI
files, etc).
There is no need to exhaustively test every capability of a
third-party library -- unless you are publishing all of that
capability as part of your system. More likely, your system only
uses a fraction of the third-party library's functionality. Test
that, either directly, or indirectly by means of the test cases
that verify the dependent component's behaviour. Better still,
wrap the third-party library in a new facade component and test
that. It provides a more controllable dependency, and may even
permit replacement of the third-party library by one from a
different vendor should conditions demand it. The same reasoning
holds for other third-party items such as SQL statements,
database drivers, and so on.
Don't worry about testing functionality in any given order,
e.g. from least complex to most complex, or in reverse dependency
order -- just test!