From Test Aversion to TDD Enthusiasm: A Developer’s Journey
How treating tests as puzzles transformed my development mindset and code quality

Most developers do not test their code. They test in production.
I haven’t tested my code for a long time either, when I was building projects at university. However, that changed when I started working in companies.
Suddenly, you have real people online using your software. And if something breaks, it is your responsibility to fix it.
Here, tests come into play. They act as insurance for your code, ensuring that everything works as expected and helping you catch bugs early.
There are many ways, actually, to write your tests, and it is highly dependent on the project type, programming language, framework, etc.
Over the years, I have evolved through many paradigm shifts in testing. From not testing at all, to writing tests after finishing the implementation, and now practising Test-Driven Development (sort of).
How I wrote tests in the past
When I first started writing tests, I would typically write them after completing the implementation of a feature or module.
At the beginning, it was like shooting myself in the foot, because my implementation did not account for testability.
After some time, I learnt to write code that is testable, and writing tests became easier. However, I still found myself writing tests after finishing the implementation.
However, there was always this moment when I was happy with the implementation and then I had to switch my mindset to testing. This often felt like a chore, and I would sometimes rush through the tests just to get them done.
Transition to Test-Driven Development (TDD)
I heard a lot about TDD, but I was sceptical at first. Writing tests before the actual code seemed counterintuitive to me. However, now someone has given me a convincing argument to try it out.
It was:
Treat it as a game. You write a test that fails - it gives you a puzzle to solve. Then you write the code to make the test pass - you solved the puzzle! Repeat.
It can trick your brain into enjoying the process of writing tests and code, since you have dopamine hits from solving puzzles.
Of course, it was hard at the beginning. Writing tests first felt unnatural, and I struggled to come up with tests before having the implementation in place.
But thanks to my experience with writing tests after implementation, I was able to adapt quickly.
Suddenly, the tests and the code started to come together at the same time. Writing tests first helped me clarify my thoughts about the implementation, and I found that I was writing cleaner, more modular code.
A Couple of Tips and Tricks
For the sake of this article, I mainly talk about unit tests, but the principles can be applied to other types of tests as well.
I have a couple of paradigms when writing tests in general, and they are also applicable to any testing approach in my opinion.
Proper naming
The name of the test should clearly describe what is being tested and under what conditions.
I would never name a test like test_functionality or test_case1. I think the test should not contain any test or case or check words in the name.
I know, some frameworks require you to prefix the test names with
test_, but if that is the case, just ignore that part.
It should be descriptive enough so that anyone reading the test name can understand what is being tested without having to read the test code itself.
I am not adamant about specific naming conventions, but as a team, you should agree on something and stick to it. If it makes sense to everyone, it is good enough.
For example, I use Given-When-Then style for naming my tests:
test("Given a user is logged in, When they update their profile, Then the changes should be saved", () {
// Given
// When
// Then
})
For Python:
def test_given_user_is_logged_in_when_they_update_profile_then_changes_should_be_saved():
# Given
# When
# Then
Actually, I do not use this convention for the sake of convention. I use it because it makes naming tests easier for me. It reduces mental overhead when writing tests. You just think about the three parts and fill them in.
It also helps you avoid writing lengthy tests, because you see the three parts and you can focus on one part at a time.
Every test should proof ideally one thing.
ZOMBIES
I have not realised it for a long time, but I tend to follow the ZOMBIES principle when writing tests.
ZOMBIES is acronym and not real zombie, as it stands for:
- Z – Zero
- O – One
- M – Many (or More complex)
- B – Boundary Behaviors
- I – Interface definition
- E – Exercise Exceptional behavior
- S – Simple Scenarios, Simple Solutions
So, when writing tests, I try to cover these aspects:
- Zero: Test the behavior of the code when there are no inputs or data.
- One: Test the behavior of the code with a single input or data point.
- Many: Test the behavior of the code with multiple inputs or data points, including complex scenarios.
- Boundary Behaviors: Test the behavior of the code at the boundaries of input ranges or conditions.
- Interface definition: Test the interactions between different components or modules.
- Exercise Exceptional behavior: Test how the code handles exceptional or error conditions.
- Simple Scenarios, Simple Solutions: Test straightforward scenarios to ensure basic functionality works as expected.
Or in other words, you are building the test suite that goes from the simplest cases to more complex ones, covering all the edge cases in between.
I found myself naturally following this approach when writing tests, even before I knew about ZOMBIES. The same goes for the TDD approach.
Take the “Zero” case first, write a test for it, make it pass, then move to “One”, and so on. It will help you build a comprehensive test suite that covers all the important aspects of your code.
Original article about ZOMBIES by James Grenning.
Refactor after tests pass
Once all tests pass, take some time to refactor the code. This includes both the implementation code and the test code.
Refactoring helps improve code readability, maintainability, and performance.
It is important to ensure that the tests still pass after refactoring, which confirms that the changes did not introduce any new bugs.
You have now bought insurance for your code, so feel free to improve it. If you break something, the tests will catch it.
Defect-Driven Development
Sometimes, instead of writing tests for new features, I write tests for known bugs or defects in the code.
I do not start fixing the bug right away. Instead, I write a test that reproduces the bug first. This test should fail initially, confirming that the bug exists.
Once the test is in place, I proceed to fix the bug in the implementation code. After making the necessary changes, I ran the test again to ensure that it now passes, indicating that the bug has been successfully resolved.
Now, this test will prevent regressions in the future. If someone makes a change that reintroduces the bug, the test will fail, alerting us to the issue.
Conclusion
I still have a long way to go with TDD, but I am convinced that it is a powerful approach to software development.
By writing tests first, I am able to clarify my thoughts about the implementation, write cleaner code, and catch bugs early.
I believe that TDD is not just a set of practices, but a mindset that can help developers become more effective and efficient.
Socials
Thanks for reading this article!
For more content like this, follow me here or on X or LinkedIn.