Test-Driven Development is an approach that provides significant value. Behavior-Driven Development is an extension of Test-Driven development that builds on that value and makes the benefit apply more broadly to teams instead of only to individuals. They are really very much the same thing with the focus placed on different scopes and with different emphasis. It is good to use these practices to execute the creation of the design (and implementation) of systems.
It’s easy to say that these things are valuable and helpful, but it’s something else to tackle the reasons for what makes it so. Please consider this post my attempt at doing so.
Test-Driven Development is a misleading name. It communicates that we are dealing with a practice focused on quality assurance. Testing is about identifying defects. Testing is about searching for weaknesses and pointing out things that are wrong or need improvement. Testing is about poking an existing machine and seeing where it breaks. Test-Driven Development is (mostly) about none of these things. It is (mostly) not a testing practice at all. It is a design methodology and a documentation effort. I say that TDD only mostly not a testing practice, because it does result in having a suite of regression tests ensuring that the system does not exhibit failures in existing functionality. It is therefore slightly a testing practice. That it results in testing, though, is accidental. The point is design and documentation, not testing. Andrew Binsock stated this exceptionally well several years ago. The purpose of test driven development is not that you test your code, but that you think about what it is supposed to do before you write it and you express your design and your intent for what the code should do by writing tests. The tests are really specifications for defining the purpose, intent, and functionality of the code.
By now, most software persons are familiar with the idea of Test-Driven Development and the core dictum that you write no application code ever unless it is for one of two purposes:
- Make a failing test pass
- Improve the quality of existing application code
This is the “test-driven development manta”. The idea is well-expressed with the formulation: “Red-Green-Refactor”. The idea is that you first write a test to define what your working code that want to write should do, see it fail because you have not yet implemented it, and then write the code that makes the test pass. Only after you have passing tests ensuring correct functionality, do you proceed to improving the code and make it more maintainable with the confidence that the tests will indicate that it still satisfies the expectations defined for it when you are finished. Refactoring is an extremely important part of Test-Driven Development in making sure the code is readable and maintainable.
Still, the question remains as to why one should approach a software project in such a way. I think the answer lies in asking the right questions. Before starting on writing some code, there are some key questions one needs to ask:
- What is this code supposed to do?
- How do I know when it does what it is supposed to do?
Without asking these questions, a coder is essentially just a drunk staggering around in the dark. She is a person without a purpose. He is someone just being busy and maybe accidentally accomplishing something,. Asking these questions, though, is how mere effort turns into targeted value generation.
What is this code supposed to do?
The answer to the first question could take one of several forms. It also depends greatly on the project and the source of the answer depends on the structure of the team(s). In most cases, there is some person or group other than the developer (and in addition to the developer) defining requirements. These requirements are documented in some form. That form may simply be a conversation or it may be more formal. The core premise of test driven development is to take the intent expressed by whatever person is requesting and defining the value to be delivered by the system and turn that into an expression of the requirement in code. There are several reasons for this. One is that expression in test code is coupled to the application implementation code in such a way that if the intent of the system changes and therefore application code changes to match the new intent without changing the executable documentation of the intent of the system, the documentation will begin to fail. Instead of a wiki page or a ticket in a tracking system failing to reflect the reality of the system in a way nobody will ever notice, a test that documents the intent of some part or use case or micro-unit of the system will fail in such a way that anyone executing the tests will see that not all of the tests pass. A failure of a test indicates either documentation that is out of date or application code that fails to address requirements of the system. Either way, that this failure is communicated is a positive development and facilitates addressing it.
How do I know when it does what it is supposed to do?
Developers, like other humans, have a tendency to keep working on something to make it perfect long after it has reached the point of being good enough. They also think of many ways to add more “value” that may or may not actually be of value. When there are tests that assert that the system does what it is supposed to do, there is a clear stopping point – a clear delivery point. When the tests all pass, the functionality is implemented (assuming the tests are complete enough to tell the story of what is desired). Test-Driven approaches foster an environment where it is straightforward to understand when implementation is complete.
A failing tests is a use case that is unsatisfied by the application or system. It tells us there is something that needs to be done. A suite of passing tests tells us that we have a system that satisfies all the use cases for the system (at least the use cases documented in an executable way). It also tells us that there is nothing to do other than refactor (improve the quality of the code without changing any functionality) or to add new use cases. Refactoring is an important part of the test-driven development cycle in that it is how one can take the simplest solution possible that makes a test case pass and turn it into something more readable and maintainable. It is also enabled by having a suite of tests that assert the desired functionality of the system. A refactoring effort can be performed confidently only if there is a way of knowing that the improvements to the code quality didn’t break the correctness of the code. Executable documentation of the desired functionality in the form of tests asserting the desired behavior is the best form of this confidence of which I am aware. Thus, upon answering the two important questions about what the system is supposed to do and how I can know when it does it, I can take the third step of being able to improve the code and design with the confidence that the code still does what is was designed to do in the first place. This is powerful. This part of the value of TDD is there as long as there are tests and doesn’t require TDD, but with TDD one is much more confident that the tests express intent and that the intent is captured than when writing tests as tests instead of using them to flesh out design. Also, the other benefits of documenting the intent of the code and knowing when it is done are only realized if done via driving the design with tests.
I like to think about what it means when bugs are discovered in code using a Test-Driven approach. When viewed through a lens of a Test-Driven/Behavior-Driven approach, there are never bugs in application code. If all the tests are passing, the code is working according to the executable specification. If there is something known intuitively to be wrong with how the code is operating despite the passing tests, it means one of two things:
- There is/are missing test(s) that should be specifying the desired behavior known only to intuition.
- The tests are wrong and don’t properly express what the system and/or unit is supposed to be doing.
Ultimately, there are no application bugs – only missing tests and bugs in tests (assuming the tests pass).
What other value is there in testing/using Test-Driven practices?
I know developers who default to using a debugger for executing their code. I think this is a travesty. Debuggers should be used sparingly because shifting in and out of running a debugger is expensive in the time that it takes and it leads to a lot of wasted testing effort that could be re-used if the effort were expressed in test code. Writing tests that exercise code is a much better way of knowing what is happning in execution and being able to verify results. Typically, if it’s worth doing something to test how the code works, it’s worth automating the test to be able to target that behavior again when something else has changed. This doesn’t necessarily means Test-Driven, but it is enhanced by documenting what the code should do with a test and using the test to verify what it does. Seeing which test is failing is often more revealing of the deficiency than trying to interpret the results of stepping through a debugger and it’s almost always more time efficient.
What about Behavior-Driven?
As I mentioned earlier, BDD is very much the same thing as Test-Driven Development, but applied at a different scope. In fact, Dan North, the creator of the term Behvior-Driven Development (of coouse, to him, it’s Behvriour), stated pretty much exactly that in response to the charge that BDD was nothing new and just the same thing as TDD. I am a huge fan of Behavior-Driven Development and tools like Cucumber and its .NET port, SpecFlow. These tools enable team collaboration on creating specifications in natural language conforming to the Domain-Specific Language syntax known as Gherkin that are parsed into executable specifications. For an in-depth look at how to use Behvior-Driven development with Gherkin-based tools, there is no source better than The Cucumber Book. It is a must read, in addition to this video by Matt Wynne, one of the authors of the book. I consider this video to be The Cucumber Book – The Movie, as in it, he goes through part of the running example from the book and shows it being built.
In short, Test-Driven Development is valuable because it causes developers to think about why they are writing their code and what it is supposed to do before writing anything. Behavior-Driven Development is Test-Driven Development applied to the entire team instead of just the developer and makes the creation of specifications that can be transformed into executable specifications into a collaborative effort and makes the documentation accessible to the entire team.