Test Driven Development is the practice of writing a test for a piece of required functionality, before writing any implementation code. This test should fail when first run, and then, you write the code to get it to pass. It doesn’t have to be the most perfect code, just so long as the test passes. Once it does, you can then safely refactor your code.
TDD is a discipline, and as such you must try your best to not be tempted to write tests after you’ve written code. Pressure can come from clients or employers to delay or not bother with the writing the tests at all. Firstly, they need to understand the benefits of tests, and then the additional benefits of TDD on top so you can secure that time to practice TDD correctly.
If your application has tests that were written after the code that implements them, that means TDD wasn’t followed. It does mean however that your project has test coverage, which is a good thing that has many benefits (and is definitely better than not having any tests), but practising TDD brings additional benefits on top of those. Often, articles on the internet claim to write about those additional benefits but instead seem to end up focusing on the benefits of tests in general. This post will try and focus strictly on the benefits of TDD.
You shouldn’t assume either that TDD doesn’t mix with BDD. The key is writing the tests before the code. When writing feature specs that define the behaviour of something, if you’re writing those specifications before implementing them, you’re TDD’ing too.
At Made, we use TDD when writing our feature specs. We use tools most commonly used for unit tests for our feature tests too, such as RSpec with Capybara helpers, rather than things like Cucumber, which are often associated with BDD (that said, you can still use Cucumber for feature specs and TDD). In all of the time that we’ve been using TDD, these are the biggest benefits we’ve noticed along the way:
1: Acceptance Criteria
When writing some new code, you usually have a list of features that are required, or acceptance criteria that needs to be met. You can use either of these as a means to know what you need to test and then, once you’ve got that list in the form of test code, you can rest safely in the knowledge that you haven’t missed any work.
You’re more productive while coding, and TDD helps keep that productivity high by narrowing your focus. You’ll write one failing test, and focus solely on that to get it passing. It forces you to think about smaller chunks of functionality at a time rather than the application as a whole, and you can then incrementally build on a passing test, rather than trying to tackle the bigger picture from the get-go, which will probably result in more bugs, and therefore a longer development time.
Because you’re writing a test for a single piece of functionality, writing a test first means you have to think about the public interface that other code in your application needs to integrate with. You don’t think about the private methods or inner workings of what you’re about to work on. From the perspective of the test, you’re only writing method calls to test the public methods. This means that code will read well and make more sense.
4: Tidier Code
Continuing on from the point above, your tests are only interfacing with public methods, so you have a much better idea of what can be made private, meaning you don’t accidentally expose methods that don’t need to be public. If you weren’t TDD’ing, and you made a method public, you’d then possibly have to support that in the future, meaning you’ve created extra work for yourself over a method that was only intended to be used internally in a class.
Will your new code have any dependencies? When writing your tests, you’ll be able to mock these out without really worrying about what they are doing behind the scenes, which lets you focus on the logic within the class you’re writing. An additional benefit is that the dependencies you mock would potentially be faster when running the tests, and not bring additional dependencies to your test suite, in the form of filesystems, networks, databases etc.
6: Safer Refactoring
Once you’ve got a test passing, it’s then safe to refactor it, secure in the knowledge that the test cases will have your back. If you’re having to work with legacy code, or code that someone else has written, and no tests have been written, you can still practice TDD. You needn’t have authored the original code in order for you to TDD. Rather than thinking you can only TDD code that you have written, think of it more as you can only TDD any code you are about to write. So if you inherit someone else’s untested code, before you start work, write a test that covers as much as you can. That puts you in a better position to refactor, or even to add new functionality to that code, whilst being confident that you won’t break anything.
7: Fewer Bugs
TDD results in more tests, which can often result in longer test run times. However, with better code coverage, you save time down the line that would be spent fixing bugs that have popped up and need time to figure out. This is not to say that you might be able to think of every test case, but if a bug does come up, you can still write a test first before attempting to fix the problem, to ensure that the bug won’t come up again. This also helps define what the bug actually is, as you always need reproducible steps.
8: Increasing Returns
The cost to TDD is higher at first, when compared to not writing any tests, though projects that don’t have tests written first usually end up costing more. This stems from them not having decent test code coverage, or any at all, making them more susceptible to bugs and issues, which means more time is spent in the long run fixing those. More time equals more money, which makes the project more expensive overall. Not only does TDD save time on fixing bugs, it also means that the cost to change functionality is less, because the tests act as a safety net that ensure your changes won’t break existing functionality.
9: Living Documentation
Tests can serve as documentation to a developer. If you’re unsure of how a class or library works, go and have a read through the tests. With TDD, tests usually get written for different scenarios, one of which is probably how you want to use the class. So you can see the expected inputs a method requires and what you can expect as outcome, all based on the assertions made in the test.