Welcome to the inaugural post in this “30 Days of TDD” series. If you’ve never done Test Driven Development or aren’t even sure what this "crazy TDD stuff” is all about than this is the series for you. Over the next 30 days this series of posts take you from “I can spell TDD” to being able to consider yourself a “functional” TDD developer. Of course TDD is a very deep topic and truly mastering it will take quite a bit of time, but the rewards are well worth it. Along the way I’ll be showing you how tools like JustCode and JustMock can help you in your practice of TDD.
In the first few posts of this series I’ll be reviewing a few basic software design and development concepts. These form a foundational basis for the application of TDD not only as a development methodology, but as a software design methodology. These posts will ensure that we are all starting off on the same basic page before leaping into more complicated and advanced topics. The last five posts in this series will be based on questions about TDD from the readers.
Before we can get into a discussion of exactly what TDD is and how it works, it’s important to have a common and defined understanding about what a unit test is. The term “Unit Test” is often thrown about by developers who attach only a basic abstract meaning to what a unit test is. But a unit test is a very specific type of test with a very clearly defined purpose and set of attributes. Luckily, the basic definition of a unit test is relatively simple; a unit test is a test that tests ONE specific requirement for ONE specific method. Later in this series will discuss concepts such as row tests, series tests and set tests, but in the end these are unit tests and if done properly they fit the one requirement/one method rule. If your test violates the one requirement/one method rule than that means it is some kind of test, but not a unit test. That doesn’t mean the test does not add value, as you’ll see in a future post about integration testing, it just means the test is not a unit test.
Unit tests that follow the one requirement/one method rule have the following attributes that further define them as a unit test:
· Targeted: Unit tests that test one thing (including one set of inputs) at a time are targeted.
· Isolated: The code you are testing should be isolated from other code in the application as well as any external dependencies or events
· Repeatable & Predictable: A unit test should be capable of being run over and over and assuming that the code under test and the test itself have not changed, producing the same result.
· Independent: Generally speaking you have no guarantee that your unit tests are going to run in any specific order. Nor should your tests expect or require this.
I’ll cover these attributes in more detail in future posts, but this should help establish a common understanding of what a unit test is. A lot of developers write what they think are unit tests. By the specifications here it would be fairer to refer to these as partial integration tests. As this series continues you will see how to write actual unit tests and how they differ from other types of tests.
Most developers who start out with unit testing write their application code first, then write their unit tests. This is a common, and one could even say logical first step into the TDD/unit testing world. After all, how can you have a test if there is nothing to test? Many of these developers start with the best of intentions; they are going to make sure that they are always writing their tests after they finish their code. They aren’t going to forget or put the tests on the back-burner when something more pressing comes up. Unfortunately committed unit testing after the fact is a difficult position to maintain and almost all developers eventually fall into what is referred to as TED or “Test Eventually Development.” Of course, “eventually” can be a very long time. Sometimes “eventually” never comes at all.
The first “D” in TDD stands for Driven. The idea is that the first thing a developer does is write a test based on the current specification they are working on. This test should fail as the functionality is relies on does not exist yet. The developer’s job is to then write the simplest code that will make the test pass. If more specifications exist, write more tests and repeat the cycle refactoring and refining your code as you go. If all the specifications have tests and all the tests pass, you are done. Ship it!
The practice of TDD seems simple, but it represents a fundamental change in the way developers have been trained to approach software development. As a result it takes practice and the first few times using it may seem a bit un-natural. But most developers find that in the end it’s just writing code, only you’re writing some test code first. The most difficult aspect of TDD is keeping the discipline and continuing the practice.
There are a lot of benefits to using TDD. Some are obvious, other are not. Perhaps the most obvious benefit is that your code will have fewer defects where the code produced does not match the specifications. These will still happen from time to time, but as your practice of TDD matures these types of defects will occur less often. One type of defect that properly done TDD can almost completely eliminate are “zombie bugs” or defects that seem to be fixed, yet appear to return a few builds later. When a practitioner of TDD is assigned a bug or defect the first thing they do is write a new test that exposes the defect and watch the test fail. From there the developer follows the normal TDD workflow; write just enough code to make the test pass and keep the existing tests passing. Once this is done, assuming you’re correctly testing the condition that produces the defect, the defect should not return in a future iteration of the application. TDD and defect management will be discussed in more detail in a future post in this series.
A slightly less obvious benefit of TDD is increased code quality. A mature practice of TDD results in developers writing the simplest code possible. This code also tends to be shorter and less complex than code developed by someone not practicing TDD. Generally speaking, shorter and less complex code is higher quality code. Code that shares these two attributes tend to experience fewer defects as the implementations they provides are relatively straight forward. They are also more readable and understandable which enhances their maintainability. The code tends to be more focused on performing the task it was designed for, keeping irrelevant functionality out of the picture.
A more obscure, but equally powerful benefit of TDD is that when properly practiced, including taking appropriate measurement of the amount of code that is actually being covered by the unit tests (code coverage metrics will be discussed in a future post in this series) you will effectively eliminate dead code from your application. “Dead Code” is defined as code that exists in an application, but is never executed. The code is either in a method or class that is never invoked or is in the block of a conditional that will never execute (essentially, the code is in a block where the test condition always evaluates to “false”). This code is a blight on your application. It is literally a code based parasite; it provides no value and the distraction it creates consumes resources in the form of developer hours as this code must be “maintained” although it is never used. With TDD you are only writing the code you need to make the test pass. If the tests map to the specifications, then there is not code in the application that does not map to a specification, therefore the code created by TDD is always used when it is written. However, applications change and over time a method that may have be necessary today may be refactored out of relevance tomorrow. That’s why it’s important to monitor code coverage through the development cycle of the application. Code coverage will quickly tell you if code is not being used by a test. If code is not being used by a test there one of two answers; your either missing a test or the code is “dead code” and should be removed.
Hopefully this post had answered some questions you might have about what TDD is and why you should look into it. I hope you’ll find over the next 30 days that the practice of TDD can elevate the way your write code and design the public interfaces of your classes and libraries. In the next post we’ll review a for Object Oriented Programming basics to ensure that we all have a common understanding of the fundamental principles of software development that make TDD possible.
Continue the TDD journey: