Test Driven Development (TDD) is a phrase that is often thrown out but often misunderstood. What does it actually mean? Does it mean having 100% test coverage? How does the testing process influence the actual process of writing code? How do we write tests for code that doesn’t exist yet? Let’s take a look at these questions
Let’s Drive
The important word in the name TDD is ‘Driven.’ The tests drive the code. What does this mean? Let’s start with the three laws of TDD:
Law 1: You can’t write any production code until you have first written a failing test
If testing is driving our development, that means that we don’t develop until we have a test. So what do we test? This is where the second law of TDD comes in.
Law 2: Don’t write more of a test than is necessary to fail. Not compiling is failing.
We start with the smallest piece of functionality possible, or the simplest example. It should be something that is trivial to understand what we are testing and how we want it to work. Say, for example, we were making a program to factor an integer: We want to instantiate our PrimeFactor class like so:
Now when we run our test, it fails. We don’t have a PrimeFactors class. Let’s fix that. On to law number 3
Law 3: Don’t write more code than is necessary to pass a currently failing test
Now when we run the test, it passes. This demonstrates the first two steps of the red-green-refactor process.
The Circle of Life
RED:
Red means we write a failing test. This test should inform the design of a portion of a feature we are developing. The test drives the code in a certain direction. It defines the what behind the production code you will write
GREEN:
Green means we make the test pass. This is the actual implementation, how we make the ‘what’ from the red stage happen. We don’t care as much about cleanness of code at this stage as much as we do about the code actually doing what we want it to do.
REFACTOR:
At this stage we might look at our code, and without going back into the red, we rework the code to improve readability, improve algorithmic efficiency, remove duplication of knowledge, all without changing behavior or causing any test to fail. This step is not implemented in every iteration, as maybe we don’t know yet where duplication will exist, or maybe we don’t have a fully fleshed out algorithm yet.
In this case, we don’t have much code to refactor, as we don’t even test anything yet. So we move on to the next iteration.
GIVEN WHEN THEN FINALLY:
As mentioned, at this point we still aren’t testing anything. Let’s fix this. Tests themselves have 4 stages: Setup, Exercise, Verify, Teardown.
We start by setting up our our givens. We instantiate any needed variables and objects, and set up their internal states. We've already started on this step.
When we have everything setup, we exercise: we have our function under test do something.
Then comes the actual verification that the something we did had the expected result. This typically comes in the form of an assert statement.
Finally, we teardown: we delete anything we created for the test, close any connections to external resources, and return the system to the state it was before the test.
Looking at the fully fleshed out test we can see that it follows the pattern. Looking carefully, it might seem that there is no teardown. That is because all needed teardown from this simple example will be performed automatically by the garbage collector. Explicitly including this would needlessly complicate the test.
To fully complete our example, we can iterate through the process several more times. Each time we complete a cycle, our code is closer to meeting the requirements we established for it.
Our test format is exactly the same for each test, so I won't show all the tests, but the code evolves
until finally we look like this
Hopefully this has been useful in showing how our testing can drive our development and inform the way we code. Any feedback or suggestions for improvement are welcomed. Thanks for taking the time to read.
Ethan Whaley
Top comments (0)