This post is in response to Dr. Dobb’s editor, Andrew Binstock’s recent editorial about the universality of unit testing. I think we all can agree that it’s universally accepted that unit tests are good. But what about Test Driven Development (TDD)?
1. Better Code
Both unit testing and TDD produce better code than not using tests. To unit test a piece of code, you must decouple it from its dependencies so that they can be easily mocked. That process not only helps you write more loosely couple code, but also encourages you to reduce the number of dependencies a class may have. It will address several code smells like too many parameters in methods and constructors, feature envy (a class that uses another class too much), and too many dependencies on other classes.
Beyond that though, the process of TDD causes an incremental approach to building code. You will follow a red (failed unit test), green (passing unit test), refactor cycle many times while building a class. That opportunity to frequently refactor your code will encourage you to spend more time refactoring it. That means more time renaming identifiers to more intuitive names, more time spent thinking about the readability of your code, more time spent deciding if a given method is too large or too small, more time thinking about if this class should be split up into more than one class.
2. Easier Coding
Because you build a class incrementally in TDD, not in one shot, you only have to worry about one responsibility at a time. You don’t have to keep in your head all the things you want the class to do, all the dependencies it needs to work with, all the inputs and outputs that might break the class, all the conditions you need to check for to make sure that arguments are valid. With TDD you can take that one step at a time. And as you build each case, it often leads you to the next case, and the next one.
Although you can follow the same process without writing your unit tests first, testing after the fact doesn’t inherently encourage you to do this.
3. Solution Triangulation
Because in TDD you incrementally build an algorithm, you can essentially triangulate in to the correct solution. The bowling game kata by the illustrious Uncle Bob (google it) is a fantastic example of this. You can start with an algorithm that solves just a simple case of the problem, then slowly add more cases and refine the solution until it’s 100% correct. That makes it much easier to solve a problem than just trying to get it right the first time in one big shot.
Again test-afterwards doesn’t prevent you from following this process, but it doesn’t encourage it either. TDD practically forces you to follow this process.
4. Coding from the Client’s Viewpoint
With TDD, you write the test first. A test is a client of an object. It calls the class, consumes its return value, and must know if the process was successful. That process makes you think about the object from the client’s point of view, so you will be encouraged to make the object a good abstraction of the responsibilities that the class has. You will make its interface more clean and concise, and the method names will be more explanatory than otherwise.
5. More time thinking up front
We all know that too much up front design is a bad thing, but so is too little. On a micro scale, TDD makes you think more about the design of a particular class. You have to think about what dependencies it needs, what methods it will have, how it will interact with its collaborators, what kinds of values its methods will return. Because you can’t write a test until you have some concept of how you will call a method, and what that method will return, and how you will check its correctness, you will be prone to building better classes that are highly cohesive and follow the Single Responsibility Principle. This point is closely related to the previous one.
Testing afterwards doesn’t encourage this practice. Again you’re free to do it, but as a practice you aren’t encouraged to think about a class because you can just start coding and worry about that stuff as the class gets built.
6. Better Unit Test Coverage
Although unit testing means writing unit tests to cover your code, there are so many variations that code can take. I’m not talking about just the number of branches through code, but all the variations in code. If you take in a string parameter, that string can be null/undefined, empty, or contain a value; it can be overly long, it can contain special characters. Does your code truly handle all these cases? If you test after, the quantity of tests you write will trend lower because you will simply be verifying current behavior. (See the next point)
With test-after, your tests don’t help you decide what code to write, so therefore, variations on tests become less interesting and less a part of the core process
7. Sloth And the Temptation to Skip Unit Tests when Under Pressure
As humans we are lazy. It’s wired into us. If you don’t believe this, read Thinking Fast and Slow by Daniel Kahneman. When doing a task, the part of the task where we are most likely to cut corners is the end. We’re almost finished; we want to be finished; we want to check it off. So if testing is a distinct activity at the end of our current mini-task, we are far more likely to cut corners and write fewer and less comprehensive tests.
Also, when we test afterwards, because it’s at the end of the process, if we are under pressure from our bosses, product managers, or clients, or just our timeline itself, then one of the most tempting corners to cut is the testing. If you’ve already seen that your code is working correctly, then skipping tests for that code is extremely tempting when the pressure to ship is applied. With TDD, that is impossible. You can’t write code without testing it first, so you can’t skip out on the tests regardless of the pressure.
8. No Extra Code
With TDD, the process is to just do the minimum amount of coding to make the test pass. This becomes an art form since you must learn to write appropriate tests that cause you to write sufficient code. But when your tests are all passing, and you’ve cleaned up the code to be as well-factored as you want it, then you’re done. The temptation to write more code that handles some possible future requirement (YAGNI) is diminished since you’re only writing tests to satisfy known requirements of the code.
Testing afterwards doesn’t guide how you write your code, with the exception of making it testable. So it gives you no encouragement to write only the code that you need and no more.
In summary, the difference between TDD and simply writing unit tests is big. Don’t let the commonalities make you think that you’re getting pretty much all the benefits because you write unit tests. If you’re not doing TDD you’re missing out on so much more.