As a big advocate of Test-Driven Development (TDD), I have spent a good amount of time wondering why 95% of my conversations about TDD with other engineers go something like this:
āYes, TDD is clearly the right way to build software. But I donāt actually do it.ā
At this point, the usual reasons and explanations are laid out:
āItās hard to know what tests to write until I have written the code.ā
āHow can I know how the tests should work if I donāt know how the code works?ā
It is true that if you are testing something that uses completely unfamiliar technology, you probably donāt know yet how to go about testing it. Most of the time, though, the technology is perfectly familiar. Itās not the tools that are unfamiliar and scary - itās the product that you are trying to build.
Enter my āØOne Simple Trickā¢āØā¦
Forget about the code completely (for now). All of it. Donāt think about the implementation, and donāt think about the test code either.
Instead, just write a plan for how the product should work. Hereās how I do it:
Step 1
Start by writing simple comments in human language. Write as many as you can think of, with one per requirement, acceptance criteria, scenario, edge case, or error. Hereās an example:
// animals.test.ts
// Plan for new API endpoint: GET /animals
// returns a list of animals with the ID and name
// paginated 10 at a time by default
// can change the pagination size
// optionally filters by species type (in the query params)
// returns a 403 error if not authenticated
Of course, the code has to happen at some point. But by starting with comments like this, it removes all of the psychological resistance to the idea of writing tests, allowing you to focus on what youāre trying to build.
Step 2
Write the actual tests one by one by converting each comment into an assertion.
// animals.test.ts
import { request } from "supertest";
import { app } from "./app";
it('returns a list of animals with the ID and name', async () => {
const result = await request(app).get('/animals');
result.forEach(animal => {
expect(animal).toEqual({
id: expect.any(String),
name: expect.any(String),
});
});
})
// paginated 10 at a time by default
// can change the pagination size
// optionally filters by species type (in the query params)
// returns a 403 error if not authenticated
It is worth noting that this technique is best paired with the philosophy of Write Tests. Not Too Many. Mostly Integration. While the ācomments-firstā approach can be applied to unit tests, it is much easier to start with higher-level, more product-focused integration tests.
Step 3
Write the bare minimum application code to make the first test pass.
For example, to make this first test pass, you could return this hardcoded array from the API endpoint:
// animals.ts
res.send([{ id: "foo", name: "bar"}]);
Step 4
Clearly, this isnāt enough. The urge to immediately write more code is strong.
This critical moment is the final hurdle to overcome, and it is the hardest. Resist the urge! Stop, take a deep breath, and look back at your commented plan. Is there anything on that list that will force you to change the code to fix the glaring issue that youāve seen?
If yes, great! Keep going, it will be fixed soon.
If not, you have two options:
- Improve your current test to have more specific checks
- Add another comment to your plan for the missing requirement
Step 5
Repeat the process. Write the next test, check that it fails, and then write the minimum code to make it pass.
Thatās it.
Congratulations, you just TDDāed! It will be worth it, I promise.
Thank you for reading. This is the first of my new series, Dev Diaries, where I will be writing about something Iāve done, something Iāve learned, or something that Iāve found interesting that day.