Test Driven Development (TDD) is method from Extreme Programming (XP) that reverses the traditional order of coding and then testing. It works like this: Before you add a feature to a module, you devise a test that will demonstrate that the module properly supports that feature. You run the test, and it fails, since you haven’t implemented the feature yet. You code up the feature, then run the test again, and this time it works. Then you go onto the next feature, repeating the same procedure, but running the previous test as well as the new test. You continue this cycle, building up a collection of tests as you add features to the module.
Now, why would anyone want work in this backwards fashion? After all, how do you even know what to test for before you have written the code?
That last question is a clue that something is wrong, and helps explain the reason for TDD.
In Agile development, each user story is required to have acceptance criteria, which are a means to answer the question “Did this coding change satisfy what the story asked for?” For example, consider a story that says “Users signing on to the system should be authenticated.” There are all manner of solutions that could meet this criterion, and not all of them would satisfy what the author of the story wanted. On the other hand, consider a story with these acceptance criteria:
- Sign onto the system, using a user ID and password that are in the user database. The signon should be successful.
- Sign onto the system, passing in an OAuth 2.0 bearer token from Facebook. The signon should be successful.
- Attempt to sign onto the system with an invalid user ID or password. The signon should fail.
- Attempt to sign onto the system with an invalid OAuth 2.0 bearer token. The signon should fail.
- Perform three failed signon attempts, then attempt to sign on with a valid user ID and password. The attempt should fail until 30 minutes have passed.
These acceptance criteria give us a much clearer target to code for. TDD takes this a step further, by developing acceptance criteria for each step of the development, in the form of tests. You add tests and features in very small steps. Sometimes they feel unnaturally small, but they keep you thinking about edge conditions, as each test suggests the next one. Suppose, for example, you are writing a routine to format money. You start with a test that passes in a number, and when you implement the change, the output looks good. “Hmmm,” you say. “What if the number is negative?” So you write a test to that, and the result does not look so good. So you change the code to do something sane with negative numbers, like displaying an error message, and you run the test again. “But what if the number is really big?” you think, so you write another test, and so on. Each time you do this, you write the simplest code that will pass the test, then you refactor it, if necessary, to make it cleaner and to avoid duplicate code. You can do this safely, because with the suite of tests you build up, you can be assured that the refactoring has not altered the behavior of the module.
One of the best resources I have found for starting out with TDD is Test-Driven Development by Example, by Kent Beck. The Part I of the book, chapters 1 through 17, take a fictitious application for managing bond portfolios and add to it the ability to handle multiple currencies. The author leads us through the test-code-test-refactor-test cycle little by little, showing how one test suggests another, and how it is possible to vary the size of our steps from baby steps, when things are confusing, to large steps when things are more clear.
Part II of the book, chapters 18 through 24, uses TDD to develop an xUnit harness in the Python language. xUnits are test harnesses that make TDD go much faster by keeping track of the tests for each module, running them, comparing their output with the expected output, and indicating which tests, if any, failed. This makes TDD much more painless than if one had to run each test manually and visually verify the results. xUnit is a generic term for TDD testing harnesses. Examples of more specific ones are JUnit for Java, CppUnit for C++, NUnit for .Net, and UUnit for Unity. This section of the book, in addition to giving a practical demonstration of TDD, also imparts a good understanding of how xUnit harnesses work, and is invaluable for anyone who needs to write one for a language that is not currently supported.
Part III of the book, chapters 25 through 32, deals with design patterns for tests, and for getting tests working. This includes answers to fundamental questions such as:
- What do we mean by testing?
- When do we test?
- How do we choose what logic to test?
- How do we choose what data to test?
It also includes special testing considerations, such as mocks (programs that simulate slow or hard-to-set-up things such as database calls, in order that the tests can run quickly), and fakes (a method used in some complicated cases where you start by hard-coding the result you want, which tests the test itself, before refactoring the code to do the desired processing).
The book is a good introduction to TDD, and does a good job of explaining the rationale, demonstrating how it is done, and giving you tools to help get you started.
My favorite quote from the book is “I taught Bethany, my oldest daughter, TDD as her first programming style when she was about age 12. She thinks you can’t type in code unless there is a broken test. The rest of us have to muddle through reminding ourselves to write the tests.”
The book is available from Amazon here.