Test-driven development (TDD) is an incremental approach to software development that requires developers to write an automated test before they write production code. The rules are simple and easy to follow, but may seem restrictive if you aren’t used to writing automated tests.
I believe you will find that the discipline involved in this practice actually gives you greater freedom to update or refactor existing modules later. It will also give you greater confidence that the code you produced actually works.
The rules of test-driven development
- Do not write any production code unless it is to make a unit test pass.
- When writing your unit tests, only write enough test code to produce a failure (compilation errors constitute a failing test).
- Only write enough production code to make the test pass and no more. (This step prevents over-engineering a solution. Once you have enough code to pass a test you stop.)
Refactor only when all your tests are passing. (Refactoring is only supposed to change the code structure, not the behavior)
Another way of describing the rules of TDD is to use the phrase “red-green-refactor.” In the red step, you write a failing test (testing frameworks like NUnit have a visual indicator beside each test that turns red if the test is failing). In the green step, you make your test pass by writing some production code. Running your test should change the red indicator to green. In the refactor step, you have the liberty to change the design of your system without adding to or changing the behavior. Then you repeat the whole process by writing another failing unit test.
Seven benefits of using TDD
TDD requires me to understand the requirements of my system well enough to write a good test that asserts some small subset of system behavior. How can I know for sure that my system has fulfilled a requirement if I cannot test that requirement? Once I understand what I need the system to do, I must then understand how to write the test that asserts proper behavior. Failing tests continually inform me of the next incremental step I need to take in my code. The failure reason tells me what my code is not yet doing that it needs to do.
2- Instant validation of correctness
3- Incremental changes
Test-driven development requires me to write my production code in small batches. This ensures I don’t go too long with my code in a non-working state. If things get wonky and I need to start over, how much work am I going to lose? One unit test’s worth of work. No biggie. This is much preferred to losing hours worth of work and wasting all the time spent debugging only to realize I just cannot correct what went wrong.
In TDD, when I’m finished coding, my series of tests completely describe the behavior of the module of code I’ve written. That helps me evaluate my code, and helps other developers who may work on my code later understand what I’ve created. Empathy is one of the more important software engineer personality traits, and test-driven development works hand-in-hand with empathetic design for your users and coworkers. A test module created by test-driven development is actually a great design spec for your production module. And the best part is, it never gets out of date like a text document would.
5- Baked-in motivation
There’s the alternative way of writing code, which is basically, “Meh, I’ll just write the code first and the test later.” But when you test after writing code, it’s easy to skip part of the code that needs to be tested. How do you know you have covered every applicable use case? You have to keep track as you go, and you can do that with a unit test. It is essentially a pre-written test that tells you which pieces of functionality are causing issues as you write the code.
Testing after writing code actually requires more analytical thought than testing as you write, and it’s not as fun. I’ve developed code for a long time and I can tell you after you finish a project, you want to move on to the next one because you’re drained from the last one. The motivation to bug test isn’t there, not like the motivation for a new challenge. Baking the testing into the process helps retain the positive employee engagement benefits, and avoid burn-out.
6- Fear-free changes
When you write code without testing, there’s fear involved. You think, “This code is garbage and I really want to change it, but I can’t because so much is going on here and I don’t know what’s going to break!”
TDD gives you the confidence to go into the codebase and add to it. If you break existing functionality, the tests will tell you it’s broken and where. A suite of tests is really a safety net. Well-written tests give you instant feedback that your code is in a working state.
7- Encourages loose coupling of modules
This is a simplistic example of how TDD leads you to write loosely coupled modules:
public class ClassUnderTest
…..public bool MethodUnderTest()
…………var theThing = new Thing();
…………var result = theThing.DoSomething();
This is an example of tight coupling between ClassUnderTest and the Thing class. If the DoSomething() method needs to change, I might need to change ClassUnderTest as well, which is undesired.
If I write a test for this method, it would not just assert the behavior of ClassUnderTest, but also the Thing class. If a test of MethodUnderTest fails, it could be because the method has a bug, or it could be because Thing.DoSomething() has a bug. So how can I ensure a test for MethodUnderTest() does not depend upon the Thing class? Normally I would mock/fake the Thing.DoSomething() method. But because I create the Thing object inside the method there is no way to inject a mock or fake object.
Here is a better practice. I’m using dependency injection to pass in a Thing. Now I can mock the call to DoSomething() and I eliminate the dependency on the Thing class.
public bool MethodUnderTest(Thing theThing)
var result = theThing.DoSomething();
Writing tests first causes you to think about issues like this beforehand so you would not write code like in the first example. Test after development (TAD) does nothing to encourage loosely coupled modules.
Testing as you write code might seem like it will slow down your process. But if you leave out automated tests on the front end you will spend more time later debugging and fixing the code that doesn’t work. In all likelihood, test driving will save you time. It also saves other developers time when they need to change a module you created. And they can do so with instant feedback from your tests if their change breaks existing functionality.
Finally, TDD will give you confidence that your code works according to spec because your tests are the spec, and they can validate correctness in a matter of seconds/minutes.
At Patriot Software, we strive to be empathetic to the end user in everything we do. That means we’re thinking about what is going to make the end user’s experience be the best possible experience. To do this, we do as much quality control work on the front end as possible. Thus, test-driven development is just the first step in ensuring our end users love our accounting and payroll software.Why should you practice Test-Driven Development? It helps ensure your code actually works. Steven Banks explains.