RIP Test Driven Development
There is a misconception on what test driven development really is. We all know what it is, but at the same time, we don’t really.
Usually it’s the word ‘test ’ that throws us off. So we end up writing long convoluted test units, sometimes the test code being more than the actual code itself. It’s all fun and games in the first iteration but if something needs to be changed, then massive refactoring of the test units is required before you can do anything in particular. This is often the first sign that TDD has been implemented incorrectly.
By the third round, we developers simply throw our hands up towards testing our code and decide to just go agile instead. We collapse under the weight of the tests that are supposed to save us time and ensure the robustness of our final product.
“RIP TDD” we whisper, then never speak of it again.
But test driven development isn’t that bad, it’s just badly misunderstood.
Crashing into the idea behind TDD
What many of us tend to do when we first encounter test driven programming is that we write tests for every possible method and classes. This often causes an issue down the line, if those methods become redundant or need to change.
This is not TDD. This is over-abstraction of what TDD is supposed to be.
“Code that communicates its purpose is very important.”
— Martin Fowler, Refactoring: Improving the Design of Existing Code
The purpose of any piece of software is to achieve a specific end result, with contingencies when things don’t happen in the way its supposed to. They are scenarios that are generated and consumed by the end user. For the backend, that end user is the frontend. For the frontend, the end user is the customer.
When software is test driven, it means that the development workflow is focused on producing a set of particular results that emulate the needs of its consumers. It’s about fulfilling requirements — not creating requirements for how to structure and produce your code. When you do the latter, your test suite is brittle to change.
But we don’t want that. We want tests that are able to withstand change because we want software that isn’t going to break if we need to make a change. Tests are supposed to indicate how robust our code is, not how tightly coupled it is to the testing units.
At its simplest, proper TDD is an abstraction of user requirements. But what does this mean?
The language of TDD
The purpose of software is the fulfillment of a specific set of requirements. We often forget this fundamental purpose when we write our test units.
The purpose of code is to translate those requirements into a language that is later translated into bits and bytes. Code is the tip of the ice berg that we want to sculpt into something that is clear and understandable for other developers. Test driven development creates the scaffold for what we want our end result to look like.
This means writing tests in a way that communicates this in normal human language.
test if x will be returned when a and b are added together
The above language is not useful to anyone, unless you’re building a calculator. But this kind of language is present in a lot of test code that specifically targets methods and therefore coupling it with how a particular class may work.
test if items are added to the cart when triggered
This test is much more information rich and gives the data shape. It doesn’t look at how things are implemented, rather, it only cares about the final input and output.
items has to be a certain shape in order for
cart to work. In contrast to the first test, this is much more robust because unless requirements for what
items looks like changes, the test itself doesn’t need to change.
Code under the hood however, is fluid and scoped to the boundaries of the test.
test if confirmation modal appears when submit button is clicked
This test case is geared towards a frontend facing software. The above test doesn’t care how you got there, only that you got there. This makes it a flexible test case to implement as any changes behind the scenes won’t break the testing suites.
But isn’t that just cheating? Shouldn’t we be testing everything?
Yes and no.
When writing tests, your mental models should be guided by creating tests that prove your requirements have been met. Testing if a certain function works or not needs to be assessed in how it sits in the relationship with the actual feature you’re working on.
If it has a certain degree of separation, then your test is probably too coupled with the actual code.
Your test unit shouldn’t be determining what kind of patterns to use or what the final implementation should look like. It should only concern itself with the final expected outcome based on a specific scenario. A robust test suite is a collection of tests that captures the specification — it sidesteps use cases. It makes you think about exceptions and errors, and what will happen if the sequence of events or input isn’t quite what is expected.
The issue with TDD is that many new to testing tend to over-abstract rather than focus on the structure. This is like hammering in the nails without seeing how it is all supposed to come together. By the time you finish, you might come to realize that your foundation isn’t quite right and have to go back to square one and write the tests all over again.
However, if you figure out what behaviors to expect, when and where, then you are able to construct a bigger picture — kind of thinking through your tests.
Thinking in test driven development is like how a mechanic would test a car. The engineer designs it, the factory machines build it, but your local mechanic is the one that goes through a series of tests to check if the car is road worthy.
He only starts to abstract into the finer details if something is not quite right. If the headlights aren’t working, then he’ll ask what kind of bulb needs to be replaced. Replaced the bulb and it’s still not working? Maybe it’s the wiring. You get the gist.
This is basically TDD — testing to see if your software produces an expected, higher level and clearly specified expected result. The mechanic doesn’t make the parts, he only uses them. As developers, we need to learn how to think like the mechanic when writing test suites for TDD.
Then we can put our engineering hats back on and figure out what those parts look like and how they are to be made. If the parts break easily, there’s a defect in the design. The test merely catches the break — it doesn’t and shouldn’t dictate the design.
That’s basically the core principle behind TDD.