Wednesday, December 29, 2010

TDD – But What do I Test?

I am a huge proponent of Test Driven Development (TDD) and have written blogs and given presentations on the subject. The one question that is asked most frequently is “what do I test?”. As this profession is rife with the ubiquitous and inescapable use of acronyms, I too shall add to the growing lexicon with two that help to answer the “what do I test” question - FAST and BEER.

FAST FAST is not actually an answer to the question, but a setup. FAST describes what makes a good test or suite of tests.

Fast – yes, the word associated with the first letter is the same and the acronym itself. Even a unit test project with thousands of tests should take no longer than a few seconds to run.

Atomic – unit tests should test one and only one piece of functionality or behavior. Atomic also means the test is not dependent on outside systems such as a web server or service, the file system, or a database. Those should all be represented by abstractions to support the F in FAST. We should be able to write and test our domain without a UI or data store.

Static – once a test is written, it should not change. This does not mean tests can never be refactored, but if you are practicing TDD, then you are setting up expectations of how the system should behave before writing any code. The implementation of that expectation may and probably will change, but the underlying test for the behavior should remain unchanged.

Thorough – unit tests should test as many conditions as the system under test will face during its lifetime. Testing just a single passing case does not really exercise the code fully or completely. It may not be possible for us to test every edge case, but we can put the code through its paces and ensure its behavior, stability, effectiveness, and graceful failing.

BEER Now on to what needs to be tested. BEER describes a set of cases or circumstances that require reflection and attention when writing tests and designing our APIs. The B in BEER does not stands for behavior - that is just a little too vague to be instructive.

Boundary Conditions – a value that exceeds the maximum or minimum allowed value. If we are testing a validation rule for a string that is required and cannot be more than fifty characters, we need to write a series of tests that address each a series of conditions - a null string, a zero-length string, a string with one character, a string with a length greater than one and less than fifty, a string with fifty characters, and a string with more than fifty characters. The same would apply to numbers or dates. This follows the T in FAST.

Existence – Not Cartesian existence, but nulls and state. We often see this when testing repositories and services. Unit testing persistence requires inserting an object into the data store (represented by an abstractions such as a repository - remember the A in FAST) and retrieving it to make sure it was correctly saved. The opposite, removing an object from the data store, requires a test to assert that it was correctly deleted. This also applies to an object’s state. We want to write tests for state transitions and verify that our objects behave appropriately in each case.

Exceptions – although we may not be able to foresee all possible conditions, we can test for certain exception conditions and ensure they are handled gracefully. Does an object in an invalid state throw an exception? Does the exception give the user an out? Does the exception provide useful information to either the user or other developer for resolving the issue?

Range Conditions – This would include things like a start date occurring before an end date or an invalid enumerator or case in a switch statement. Testing a sorting algorithm where the first element is less than the proceeding one would fall under range conditions.

Conclusion Unit testing is not just about one case or single event. It is about proving our code works in different environments and conditions. Is is also about falsifiability. When we are writing tests, focus on the negative cases and taking a defensive coding posture. What can go wrong and how will my code react? When outside forces assert pressure on the system how will it handle them?

Following these guidelines will help you achieve not just higher test coverage, but higher quality test coverage.

4 comments:

  1. Nicely said, though I'm not sure "static" is the right description. Our tests should evolve as our understanding of the domain improves. Use "R" for "relevant" instead of "S" for "static" to keep our tests relevant to our domain and the unit they are exercising. It also has the nice acronym of:

    FART BEER

    Enjoy!

    ReplyDelete
  2. Good things to consider on my path toward practicing TDD. I must say, I really enjoyed your Zen Coding presentation at SQL Saturday in Houston--it inspired me to adopt TDD. Keep up the great work!

    ReplyDelete
  3. I appreciate the compliment and the fact that you were inspired by the presentation. We need more passionate, motivated developers.

    ReplyDelete
  4. Gratified to see a blog which reinforces my reasons for incorporating defensive programming with TDD.

    Nice to know there is a kindred spirit :)

    ReplyDelete