A Few Words About the Motivation Behind This Article…
The idea about this article came from a discussion I had with various devs during a GDG meetup in Belo Horizonte, Brazil. After lots of arguments on many subjects—including a Ruby/Java discussion about which is better—I asked someone to prove to me that TDD is really a good approach for software development. The following piece is about my conclusions based on what we discussed.
TDD Is More Than Just Test Before Code
TDD, or Test Driven Development, is one of the most widely used approaches for agile development nowadays. This is because agile development relies on implementing small pieces of software in each cycle (commonly called sprints). So, it makes sense to automate the tests of each part to return the desired result—a final release that is reliable—after a number of iteration cycles needed for its full implementation. There is no question about the importance of tests to receive the desired result, but TDD is not just about testing and the order in which you do it, it's also about planning and documenting all you are doing for each implemented piece of code required for the overall functionality.
TDD itself is a cycled approach. In its flow, we start a new feature by writing a driver test—which should fail given the code is not ready to make it pass—then write our simplest version of the code to make it happen, and finally, refactor everything to improve test/code. Each cycle includes write/improve test, write/improve code, and then the refactor phase until we have a final version of both test and code.
*Let's assume by Tests that we're talking about Unit Tests. There have been discussions about TDD and Unit Tests—that they are not the same, something I'd agree with. Driving your development by tests doesn't mean using only Unit Tests for the task. Other tests can include Integration and/or End-to-End Tests. But, to make it straightforward, given the scope of responsibilities of a pure developer, Unit Tests makes more sense in this thesis.*
Testable code is not simple to write and, most of the time, you need to refactor the implementation, and also even the architecture, to be able to test it easily. That's because unit tests need to be isolated—that is, for an object oriented language, all classes should only be tested for their own provided behavior. This way, for instance, if a class A depends on another class B, we should not rely on B's results during A's tests, requiring some mocked objects of B to test A. This requirement implies a dependency injection endpoint available on A to make it run with test resources.
In a non-TDD development flow, it is easy to hard couple A and B classes without taking in account the test you must write after it. Using TDD we can reduce this risk, thinking in advance about how coupled our classes are and providing better and easier-to-maintain code, which is one of the most important concerns in agile development since we may need to change it in the further iterations.
Also, TDD improves our abilities to follow KISS (keep it simple, stupid) principle, since we are just worried about what we need that time to make the test pass, and not, what we may need in the future. This makes it easier to have a good coverage of the scenarios you need to handle.
If you're like myself, an Object Oriented developer, you also should have concern about how S.O.L.I.D. your code is. Following those principles guarantee that the resulting code is as maintainable as it is possible, which is really important, as said before. S.O.L.I.D. principles can become natural over the time, but making sure your code follows them, sometimes require a planning phase, which can be merged with the first phase of TDD, since it'd give you all the details and architecture your code would have after implemented.
To be more specific, one can guarantee that his/her classes have only single responsibilities (S), that they are open for extensions, but not for modification (O), that they can be replaced for their subtypes (L), that all interfaces have their own scope and that it is as narrow as possible (I), and that all dependencies should rely on abstractions instead of concretions (D). All of this with a planning phase using TDD.
Although most of us could (and many may!) argue against this point, TDD is more natural that we think. Every time we need to implement something new, we need to set its contract, that is, what it should follow given the actual structure of existent code. This way, we first decide what its inputs and outputs will be. We plan how it should behave. We plan what it should use to complete its task(s). But, we do all of this without the benefit of the test itself. That's why TDD is a better approach if you are planning to have tests in your code, because, in the end, it gives you less work. The only thing we need to change our minds about is that we should not be so eager about the result in the first place, but about how we are planning to get to that result. This is a really important phase of any development work and, when we do this planning—using the same tool we are about to use to implement what we want—it is even more natural.
Of course, there aren't only pros. TDD, as any other approach for software development, has its own impactful cons. For instance, sometimes it is just hard to figure out where to start with a new project. Some structure needs to be in place before starting testing features itself. Still, all developers of the team must be fluent in the test framework they are using, which makes the project learning curve steeper. Other costs on productivity rely on setups, some slow tests scenarios, and fixing brittle test cases. But, given you are going to handle all of this and many other costs just because you are going to use any testing approach, we can say that this is inevitable. And, with enough time and effort, your tests become easier to write, given your improved code and experience. Again, even with some cons, TDD still seems worth it.
I want to conclude by saying that I am not a TDD "evangelist." I'm not actually a full user of this approach myself. Sometimes it just doesn't make sense using it—maybe because the project is only about some simple code to do a simple task, what we'd call a tool. But, since having this meetup discussion, I am thinking more about some other situations, like sharing my tool code with other developers and learning how it could be improved in the future to extend its scope. With this perspective, TDD becomes a good way to do it, helping to create a better software structure for any further implementations. In the end, as I have already said, it requires some kind of change in thought process and that, by itself, requires work. The important thing is that this discussion proved to me that it really is an excellent approach to improve the code in my applications—and that is never a bad idea.