What the heck is this TDD thing?
TDD (Test Driven Development) is exactly as it sounds: using and writing tests to drive the development process; shaping the software, and the test suite at the same time, one test at a time.
TDD offers something akin to a “paint by numbers” approach to software development — simply map out the tests you need to write, implement the code and move on to the next test.
Eventually you get a work of art — and you don’t have to know what the end result looks like to do it, just follow the map in front of you (your tests).
TDD in a nut shell:
- Write a test for a given unit of functionality / feature.
- Run the test; ensure your newly added test fails.
- Write the minimum amount of code for your test to pass (don’t care about code quality here) — run the test and ensure it passes.
- Refactor your code (now care about code quality again!);
re-run your tests to ensure your code still passes.
THEN Repeat from step 1.
Why would anyone do this! What are the benefits?
Ensuring the cultivation of a test suite happens right through a project.
This is in antithesis to building and retrofitting tests at the end of a development cycle where there is then the potential for quality compromising conversations to creep in:
“Do we need this level of testing here? We need to get this feature out! …we can always revisit it after all!”.
Sound familiar? 😉.
Tests that are written were designed to fail.
The tests were proven to fail if code didn’t exist to make them pass — before any actual code was written.
This makes for an ultimately more robust test suite — one which protects unknowing developers from mistakes that could catch them out by catching breaking changes early in a less expensive part of the development process. This is exactly what a test suite should do - not just be there to mindlessly satisfy a few static code analysis tools.
Code developed with TDD is easier to refactor.
Because code has been designed and written to be testable, and because there is already a robust test harness around the code, it is simply much easier to refactor said code into something else.
Keeping the developer on the right train of thought.
This benefit of TDD is rarely mentioned but is probably the most valuable.
While working on implementing a large feature which is quite cross-cutting across multiple application layers, it is easy to get lost in the detail about what the thing should do and how this other thing should talk to the first thing
“What if that thing passes a thing to the thing — did we cover that with tests at this point in the thing?”.
Just think through the next job that your class / utility / service has to do and write a test for it — then implement it.
…Okay, well I’ve used TDD before and it just means lots of useless unit tests I have to maintain
TDD isn’t just about writing unit tests.
The methodology is most often applied to unit tests because unit tests themselves are inherently simple, therefore they are easier to apply a structured approach around. One of the biggest criticisms of the discipline is that you just end up with lots of “noddy” unit test code like that below:
public void accountDTO_constructorWithProperties_hasProperties()
AccountDTO actual = new AccountDTO(1L, "Account");
assertThat(actual, hasItem(hasProperty("id", equalTo(1L))));
}@Test(expected = IllegalArgumentException.class)
public void accountDTO_constructorWithNullProps_throwsException()
AccountDTO actual = new AccountDTO(null, null);
The above tests can be a bit burdensome — add more properties to this class and we have to either expand out the constructor with more properties or add a new constructor, this means more tests. They are also not really testing anything useful and no real business logic here.
While tests like this are necessary evils in certain cases — they can also just mean the creation of more overhead when refactoring — this often puts people off the approach because more of these sorts of tests are often created as a result.
In these cases, the emphasis is simply on the developer to write good tests that are relevant to the domain. You should write tests that will actually protect the business logic from the threat of a naive developer who doesn’t know it, rather than generating more compiler errors for a future developer to overcome.
TDD in isolation doesn’t teach us anything about how to write good tests, it only advocates the simple principle that writing good tests first should lead to writing good quality code.
TDD can be applied anywhere in the testing pyramid: at the unit test level, integration or acceptance test level.
Some projects, technologies and frameworks are more challenging than others to apply the approach, but there is never a situation where it’s impossible, it can just be impractical in some cases.
Hopefully after reading this you will think about the project you’re currently tackling, (or about to tackle) and whether leveraging TDD could be of benefit.
“Quality is never an accident; it is always the result of intelligent effort.”
— John Ruskin.
Thanks for reading — now go write some tests 😁.