Learning Test Driven Development (TDD) is hard. I have a decade+ of “Test Last” “Test After” content on my blog and YouTube, none of it “Test First” until much later in my career. So here’s a 3 phase process to ease into it. I’m assuming you already know how to setup unit testing frameworks (Jest/Vitest/Mocha/Jasmine, etc) and use assertions (expect, assert, etc).
Phase 1: Unit Testing
- Write some code you’re proud of, UI or API.
- Write a single happy path test for it and make it pass.
- Comment out the code, but don’t delete files. Use the commented out code as reference if you need to. Keep your test as is.
- Make your test pass again. Compare the code you wrote with your original code.
Phase 2: Designing Through Testing
Cool, now that you know how to write an implementation, redo the above, but hide your code in another file and close it, then DELETE your code in Step 3, keep the tests. You should have an empty file. When you make the test pass, re-open that file you hid, and compare the code you wrote vs. the new code you wrote to make the test pass.
Phase 3: Small Batches
Repeat Phase 2, but this time, try to get the test to pass with the least amount of code. If a function/class method asserts it returns true, then just return true. Track those teency additions all while keeping the test passing, whether manually or git commits. Keep going till you’re done refactoring; you’re happy with the end result. When done, compare both the code and the new code. Also, and key for this phase, try to find the largest or longest time you spent making a change; how could you make this less code in a commit and do it quicker in a smaller batch?
If you can do 1, 2, and 3, then you’re now able to refactor for as long as you like, safely.
Top comments (4)
Do you recommend TDD?
Do you always practise TDD?
If not, when do you do it or not?
Yes, I recommend it because I know of no other way to build software that you don't hate after a few weeks/months.
Yes, I always practice TDD. The only place I have not found it easy is in Elm unit tests; the compiler is so good, it's easier to change the types and let the compiler figure it out. However, I still practice TDD for acceptance tests using Cypress (Playwright could work as well). Given you rarely have logic that needs testing on the UI, I don't often need unit tests in Elm, but acceptance tests are a must.
The only time I don't is if I want to explore a new API fully. I don't mean "learn an API or language"; for that, TDD is great. Examples include:
The last part is the only one that makes me uncomfortable; if it were written in Python, then yes, I'd TDD my CICD pipeline code. I'm also super new at databases despite using professionally and playing with them for decades. In my experience the database migrations don't seem to have any testing in play, at least the Posgtres and DynamoDB ones I've used, so I have much to learn there.
Thanks for a thorough answer!
I presume the examples were examples of the only times you don’t use TDD. Even though they were quite similar to the case of learning a new language (in which case you said you use TDD).
Yup. Last week I had a super strange JavaScript bug. The tests were high level and decoupled enough that they did NOT help me understand the problem was much, much deeper. It turns out I had a very incorrect understanding of how JavaScript Array's work with empty values. I spent an hour and got so frustrated, I had to whip out the Node.js REPL and start doing basic Array comprehensions to understand what was going on.
Specifically:
Now, when I'm 60, yes, the above would probably be a test included in my test suite, and would have sussed it out. However, I had no idea what was going on, and started challenging my assumptions of how things worked. My thoughts were too jumbled and confused to make confident assertions like you do when you write the test first. I was like "Do... do I even know how Array's work? What is empty vs undefined... are they same thing?"