Test Driven Development


By Alex Allain
Testing--sometimes tedious, but certainly necessary. Maybe it should it be the basis of your approach to coding.

What is Test Driven Development?

The fundamental idea of test driven development is to write tests before writing the code to be tested. As the code is written, and you'll have immediate confirmation of whether or not a new chunk of code is completely functional, close to working, a complete disaster. In the most rigorous application, you write a single test, watch it fail, then write the implementation code that makes that single test pass, and then repeat.

Why Test Driven Development?

Writing tests before writing code tests your design--first of all, is it actually testable in the first place? But more importantly, do the interfaces you've created at actually make sense? Once you start to use a your work, you often notice subtle issues that are hard to catch during design. Writing or designing tests gives you a chance to see how someone will interact with your work, and this added perspective can be very helpful. For instance, if you were designing a string class and it takes four lines of code to convert a string into an integer, that might be an indication that your design isn't making common use cases easy. While this might come up during a discussion of the design, the consequences will be far more obvious when writing fifteen tests with the same bad interface. Tests also give you a way to measure progress of implementation. If you write your tests such that unimplemented code will fail, then you can check out implementation successes as your tests start to succeed.

Unit Tests

Test driven development usually includes a component of unit testing. Unit testing simply means testing the individual components of your program rather than just the part the user sees, which is just the operation of the program. For instance, if you have a large program composed of many different classes, including a specialized implementation of a heap algorithm with the normal heap interface:
class SpecializedHeap
{
public:
	// look at the top element
	obj* peek();
	// remove the top element
	void pop()

	add an element
	void add(obj* ele);

private:
	// implementation helpers ...
};
Moreover, you know that during the expected normal operation of your program, the user will never interact directly with the heap; its effects will be indirect, and, potentially, it may be used only rarely. For instance, perhaps your heap implementation only comes into play when the system has low-memory. As a result, testing this heap code by testing the program itself would be extremely hard to do. You'd have to come up with a use case where you can be certain that your heap will be used. If you want to test special cases in the heap implementation, you'd have to be extremely lucky or extremely clever. Why bother? This is a perfect place to write unit tests. Instead of testing your heap through the complete program, you could write functions that use the SpecializedHeap class directly:
bool test_heap_add()
{
	SpecializedHeap heap_tester;
	obj my_obj( 3 );
	heap_tester.add( my_obj );
	return heap_tester.peek()->getVal() == 3;
}
Notice that this test relies on both the add and peek functions. In general, you'd like to avoid relying on a function you haven't tested to test another function. On the other hand, sometimes you simply must test pairs of functions--or rely on very simple getters or setters. If you have a particularly complicated set of functions that must all work correctly for you test to work, you may wish to make your unit tests friends of the class being tested so that they can check the internal data structures themselves for evidence of correctness.

Test Cases

This example test case was quite simple, and it's best to start off by testing the smallest pieces of functionality first, and then build tests for more complex functionality on top of them. But you may also wish to make more complex tests for simple functions. Adding one element to a heap may work correctly, but adding three elements and removing them in order may be a mini-Waterloo.

Coupling of Code

If your program is too "tightly coupled" (i.e., interdependent), you may run into issues where you need large portions of your program working before you can start unit testing. For instance, in the heap example, if your obj class requires a lot of sophisticated initialization it may be very hard to test the heap without getting obj up and running. You might, nevertheless, like to be able to implement the heap without having a full obj class. In this case, you might decide to create a subclass of obj that has a simpler implementation, but that can provide a simplified version of the functionality needed by the heap.

Code Reuse

Unit tests also make it easier to reuse code. How's that? Let's say you didn't unit test your heap code, and you later decided that you needed exactly the same heap implementation in another program. Much to your chagrin, that program breaks once you start using your heap. You've already gotten a bit lucky in that you've isolated the problem to using the heap code, but you're still stuck figuring out exactly what is wrong with it. Now you're doing maintenance work when you'd really like to be writing a new program. By more thoroughly testing your program's pieces when you write them, you can fix problems when the code is fresh on your mind. This also means you can feel more comfortable sharing the code with others who might use parts of the code in ways that your original program never did during testing. One way to think about it is that unit tests give you a much better shot at exercising every code path, a path of execution through your code, than simple end-to-end testing.

What Doesn't Unit Testing Do?

Unit testing can't catch every problem. Issues sometimes arise in the integration of two components. Here, the program is not that the components don't work, but that they may have a fundamentally different understanding of the world. For instance, if you're using someone else's code, and you assume that their functions never frees pointers passed into them, but they do, then all of their unit tests might pass, but your code might not. Unit tests can only test that functions do what they say they do--you still have to write programs that use them in the proper way. Testing those programs is a whole new ball of wax because even if the program "works", that doesn't make it nice to look at, easy to learn, responsive or well-documented.

Regressions

Adding Features

Unit tests are great reminders of your assumptions. Every time you change the code, if an old unit test fails, that's a sign that your change has reintroduced a bug. If your change reintroduced a bug, then it's quite possible that you've forgotten some assumption about the code being tested. Perhaps you rewrote a helper function and forgot that it could take negative numbers as arguments.

Fixing Bugs

Unit tests should be a living part of your code. If you find a bug in a unit-testable part of your program, that's a good sign that you left out an important unit test. Add a unit test that would have caught that bug the first time, and you provide yourself more assurance that later modifications won't reintroduce it.

Keeping Tests Up To Date

Once you've finally got all your tests passing, it's a good idea to keep them that way. If you get too complacent about a few failing tests, you might not notice when new tests start to fail. If you really want to keep around these test cases, you might be better off creating a filter that hides them from your normal view so that new failures show up unobscured by old ones.

Conclusion

Starting your program by thinking about how to test it might seem unnatural at first, but it has a certain satisfying quality to it. As soon as you implement a feature, you get to check off a test as a new success. It's good for your psyche, and, almost as good, it reminds you when you forget to implement a feature.