Flickr photo by julyYu. License: Attribution-ShareAlike 2.0 Generic, Some rights reserved.
If you thought this is a technical post you have mistaken. The purpose of this post is to try to explain what test-driven development (TDD) is so that anyone understands it. Or at least almost anyone, with at least some technical background.
Let me start by explaining what TDD is not:
- It is not one form of software testing nor does it remove the need for that
- It is not done in order to find defects from the code
- It does not guarantee that there are no defects in the code
- It’s primary purpose is not in trying to minimize amount of defects in the code
- It does not magically solve all your problems
Instead, TDD is primarily a design method which aims in producing least amount of code which satisfies the software requirements. As an added benefit, TDD also ensures that code does what the person who wrote it intended it to do. Naturally, in case the software requirements, use cases or user stories are not right, the code might still not be doing what the user would like it to do, but this is another story and finding this kind of faults is not in scope of TDD.
So, yeah but, no but, yeah but, no but WHAT and HOW?
To begin with starting to design or implement any piece of software, developers needs to have requirements. They can be in any form popular at that time, use cases or user stories being ones often used at the moment. From a requirement, software development team plans the tasks which need to be implemented in order to fulfill the requirement.
Let’s have some example tasks:
- “Ask user’s name”
- “Ask user’s birthday”
- “Show a friendly greeting to the user”
- “Show a birthday greeting to the user”
Let’s start implementing those tasks using TDD.
First task, “Ask user’s name”. For this we probably need a method which asks for user’s name and stores it for later use. To begin, we will write a simple test in (pseudocode):
"User name should be stored after it has been entered": user_john = new User() user_john.set_name("John Doe") assert user_john.get_name() equals "John Doe"
This imaginary test doesn’t bother with how the name is asked from the user, it focuses in the storage of the gotten value and asserts that the value given is indeed stored for later use. When this test is run, it will fail as there is no other code written yet. In fact, this code would not even compile.
When a test is failing, the testing result is said to be red. This is always the starting point for adding the actual functionality. The reason for this step is to ensure that the test itself is not accidentally passing when it should fail.
Next step is to write the code to make the test pass:
class User: variable username method get_name(): return "John Doe" method set_name(variable name): return
Now the code compiles and the test passes. Still this doesn’t look like it is ready to production yet, or does it? Anyway this state of TDD cycle is called green. Only at this stage, it is allowed to refactor the code. Refactoring means changing the structure of the code without altering any functionality. It is done in order to make the code cleaner e.g. by removing any unnecessary duplication, magic strings and numbers etc. It is a necessary step to be done during development although in this example of ours there is not much to refactor. After refactorings have been done, all the tests are run again in order to make sure none of the functionality was altered or broken.
As we see, our code is not yet ready so we need to do some work with it. The TDD cycle begins from the beginning, by writing another failing test or extending an existing one. We now choose the latter alternative:
"User name should be stored after it has been entered": user_john = new User() user_jane = new User() user_john.set_name("John Doe") user_jane.set_name("Jane Doe") assert user_john.get_name() equals "John Doe" assert user_jane.get_name() equals "Jane Doe"
Now when we kick off the tests run, it will end up in failing, red state. This means we can hop on to write some functionality again:
class User: variable username method get_name(): return username method set_name(variable name): username = name
Behold, our tests are turning green again. After refactoring we would hop on to write first failing test for asking the birthday and so on and so on.
The TDD cycle is often called Red-Green-Refactor and it is essential that it is followed strictly. Only this ensures that developer writes as little production code as possible and that the code written does what it is intended to do. Refactoring step makes the code more clean, helping in maintaining that code later on. Good tests written during development also work as a splendid documentation of the code. A new developer getting familiar with code written using TDD will easily see what the developer who wrote the code intended it to do not to mention that such code is usually also easier to understand as it is cleaner. Good to remember here also that less code = less defects. So even though the main purpose of TDD is not to try to minimize the defects in the code, it sure does help in that.
If one follows the TDD discipline, the produced code will get 100% unit test coverage. This is something which is really hard, if not impossible to retro-fit in the code later on. Unit test coverage itself doesn’t make the code any better than code without any coverage, but it makes it easier, faster and more safe to change the code later. I claim that an average developer spends at most 10% of her time writing new code and rest of the time goes reading and changing old code. Developers in general do spend a lot more time reading code than writing it. So if we can make the code easier to read, it will save a lot of money in form of saved development time.