
In this post I step away a little from the purely technical angle of other articles to share something more personal: what I felt during my first days at Lean Mind and part of what I learned in the internal training we received around TDD.
Before getting into the exercises, it helps to define a few ideas that were present throughout those days.
TDD
Over the years I have heard every kind of opinion about Test-Driven Development. Some people defend it as a core engineering practice. Others see it as unnecessary overhead. My conclusion is simple: it is worth trying before judging.
TDD means writing the test that describes the desired behaviour before writing the implementation and letting the code emerge as a response to that use case. The best-known cycle is Red, Green, Refactor:
- Red: write a failing test.
- Green: implement the simplest thing that makes it pass.
- Refactor: improve the code without changing behaviour.
At first that can sound strange. Writing a test for something that does not exist yet feels unnatural. But once you practise it, you start seeing that the test forces you to define the need more clearly and prevents you from building more than the problem requires.
Pair Programming
Another practice closely connected to this way of learning is Pair Programming: two people working on the same code at the same terminal.
Usually we distinguish two roles:
- Driver: the person typing the code and applying the agreed approach.
- Navigator: the person observing, asking questions, spotting edge cases and helping maintain direction.
The real value is not in constantly pointing out small syntax mistakes. It is in sustaining a good conversation about the solution. Roles should switch often and adapt to the context, the difficulty of the task and the mental state of the pair.
It sounds simple, but doing it well takes practice, trust and a lot of human skill.
Hexagonal architecture
Another important concept for applying TDD comfortably is hexagonal architecture, also known as ports and adapters.
The goal is to decouple the core of the system from external services and infrastructure details. Instead of depending directly on tools, libraries or APIs, we create boundaries that let us swap implementations and, most importantly, test behaviour without dragging real dependencies into the test.
When that separation does not exist, especially in legacy code, TDD becomes much harder because the architecture was never prepared to be tested.
Katas
Katas are small exercises designed for practice. Their purpose is not to prove that we can reach the answer quickly, but to train a way of thinking and programming.
In fact, when we combine a kata with TDD, it is common for the solution to change every time we repeat the exercise. The valuable thing is not memorising an implementation. It is improving the path that leads to it.
Before starting
Before jumping into code, it is important to define the use cases and a sensible order for approaching them. In TDD that matters a lot because we want to move through small, concrete cases instead of assuming requirements that nobody asked for.
It also helps to keep a few practical ideas in mind:
- Do not get attached to the code. Sometimes deleting it and starting again is the right move.
- The important thing is not cleverness, but clarity.
- Git is a strong ally: small, expressive commits make progress easier to understand.
One useful habit is to record one commit when the test is red and another when it turns green. It is not a universal rule, but it makes the evolution of the work much easier to read.
Kata: String Calculator
The first exercise was to create a function that receives a string with comma-separated numbers and returns their sum. If a letter appears, it should count as 0.
The common temptation is to solve the whole problem at once. With TDD, the better question is: what are the smallest cases I need to prove first?
A possible starting list would be:
TODO:
param = '1' return 1
param = 'a' return 0
param = '' return 0
param = '1,1' return 2
param = '1,a' return 1
param = '1,1,1' return 3
The key is to solve the simplest case first and accept that the first implementation can even be a temporary hack. We are not optimising for the entire future of the code. We are optimising for the next safe step.
Later the exercise introduced a change in requirements: support a custom separator with a syntax such as //#;1#2#3. That forces the code to evolve gradually instead of being overdesigned up front.
Kata: Splitting text
In this exercise we wanted to simulate what a text editor does when it inserts line breaks according to a maximum column width. If the cut landed in the middle of a word but there was a useful blank space before it, the line break had to be placed at the space.
A possible initial list of cases would be:
TODO:
'Hola', 5 return 'Hola'
'Hola', 2 return 'Ho\nla'
'Hola Mundo', 7 return 'Hola\nMundo'
The interesting part is that, as we move forward, new questions appear. For example: what should happen if the number of columns is 0?
The important lesson is not to invent the answer ourselves when it is not part of the requirement. The right move is to clarify it with the person defining the need. In this case, we decided that the function should throw an error.
This kata also teaches another useful idea: renaming variables and making intention explicit often clarifies the path more than adding blind logic. Quite often, a recursive solution ends up being more readable than piling up loops and conditions.
Kata: Password Validator
Another exercise was to validate a password with these rules:
- It must contain at least one uppercase letter.
- It must contain at least one number.
- It must contain at least one lowercase letter.
- It must be at least 6 characters long.
Here an interesting nuance appears. In earlier exercises we started with the simplest cases. In this kata it is more useful to start from the fully valid case and then verify what happens as each rule is broken.
TODO:
param = 'Abcde1' return true
param = 'Abcdef' return false
param = 'abcde1' return false
param = 'ABCDE1' return false
param = 'Abcde' return false
That is a reminder that TDD is not a mechanical recipe. The right order depends on the problem and on which path makes the logic easiest to discover.
Conclusion
Applying TDD is not only about writing tests before code. It goes further than that: it forces us to clarify needs, challenge assumptions and let the solution evolve in the simplest possible way.
Over time I have stopped chasing the idea of a supposedly optimal solution and started valuing something else much more: a solution that is simple, matched to the need and easy for other people to understand.
That is why this approach still feels valuable to me:
- Specific tests help the code become more general and more maintainable.
- Overly ambitious code from minute one usually turns out worse.
- Practising with katas, pair programming and continuous refactoring improves both technique and thinking.
TDD does not have to be a universal obligation, but it is a tool that deserves a real opportunity. Spending time with it changes the way we approach problems and helps us build software with better judgment.