I know that there are many different reasons why a developer might pick one approach over another when tackling any given question. But here’s my line of thinking regarding tests.
There is value in writing tests after the implementation. It helps us feel more confident when the code later needs to be refactored or changed. However, “later” means that benefits are postponed. And as for me personally, postponed benefits don’t motivate me enough—I’m not THAT worried about what might happen at some point in the future. We may never even return to the code again.
In contrast, writing tests before implementation provides immediate value!
- It catches bugs in the initial implementation.
- There is no need to run the app to verify that the code satisfies the requirements.
- When something goes wrong, using debugger breakpoints is much more efficient when running only the failed test rather than the whole app.
I’m not sure if this is worth mentioning, but it kind of feels like once an interface and its tests are written, I can switch my brain off and brute-force the implementation until the tests pass. 😅
Immediate value gives me much more motivation to write tests. I don’t even need to force myself to do them because once a single test is written, it already provides value (and probably some happy hormones). So, developing a requirement can be broken down into small, satisfying cycles:
- Extract a small part of the requirements that can be tested with one or a few tests.
- Write an interface and the corresponding tests.
- Feel happy when the tests help during implementation.
- Go back to step 1.
Of course, if all the interfaces for a feature aren’t fully thought out, each time you return to step 1, you may need to rewrite not just the interface and implementation but also the tests. However:
- This approach somewhat forces us to think through the interfaces beforehand, which might not be a bad thing.
- Even if we have to go back and change things during feature development, the test suite remains. We wouldn’t have to write the tests from scratch—just copy, paste into new files, and adapt them to the new interfaces. Since tests are driven by requirements, and the requirements haven’t changed, we’d want them to pass regardless of interfaces/implementations changes.
So, in a way, we still get the long-term value of writing tests after implementation, but we also gain that value now, during the current development cycle.
Got motivated, but feel overwhelmed?
TDD does add extra work on top of implementing a feature (though all testing does). It forces you to think ahead more, which can feel overwhelming—especially when you don’t have the energy for it. So don’t push yourself too hard.
For example, when I practice this approach, I make my life easier by focusing less on other best practices like Single Responsibility Principle, uncoupled and cohesive composition etc. Once I have more experience with TDD, I’ll be able to balance both. But as long as I’m still learning, I allow myself to focus on just one thing, even if it means letting some aspects of “code quality” drift a bit.
I hope people who are reading this comment agree that tests are good: they provide a safety net to avoid mistakes, they force you to refactor the code, they serve as live documentation to code, etc. If you don’t agree that tests are good, this comment won’t make you any happier.
TDD takes testing one step further, and I believe the benefits of this outweigh the perceived effort to implement TDD. “Perceived” because I don’t believe TDD slows you down. You mentioned switching your brain off. I think it’s a great feature of TDD because it limits the context one needs to focus on. In each moment of time, the developer focuses on testing a single method or property. Numerous times it led me to add more nuanced logic to my classes because I thought of these edge cases during writing tests. So far, only benefits, and only perceived efforts.
Sometimes, I use TDD to fix bugs. When I find the code that causes the bug, I write the test that passes. Then, once it’s clear where the fault is, I change the test to reflect the correct behavior. After that, I fix the code so all the tests are green again. This works great on huge legacy codebases (and if this class isn’t tested yet, you’ll get kudos for introducing tests for this class).
And it’s an interesting point about motivation and feeling overwhelmed. I think TDD helps in that because:
So far, only benefits and no real downsides besides stopping being lazy and finding excuses for not writing tests 😀
LikeLike