It's 2026, the first day of work after a long vacation that I did everything but code; I was not even sure how to make a switch statement anymore. I checked Jira: "Delete a custom module, replace by an existing one and move all production data to a new table". 1k lines deleted later on a PR: all tests passed. PR merged, E2E phase crashed on integration. Bug fixed, moved to production, and it went live flawless 🍾.
Only two years ago, It would have taken me weeks to do it. With multiple test sessions, and It would still have not been a smooth transition, I would have chanted our old song: "we do not change working code!" after a long post-mortem session.
So what changed?
For a long time, TDD was that thing I was forced to do, so Sonarqube would be happy, I was not aware of Its benefits and how it could positively impact a project. When I did not see the value of proper testing, I let go one of the most important tools we have. The problem I was facing was simple, over mocking, over engineering, overthinking. I took everything I learned about TDD by the book and did not apply it to my reality.
I'm not alone here, the new generation today does not see TDD as a quality prof as we used to do, challenging the process or miss understanding the point of it completely, as shown in this paper. I'd like to pour my five cents into the matter and try to challenge this perception, lets make sense of tests and why we need them for
The Type of Tests I'm Using
I'm used to write tests for the given reasons: insurance, exploration, unit test or bug fix:
- ►
Insurance: When you add the test later. Basically when you know the code you are working with, so tests feel like a shore, since you need to test. But in reality this is just a policy that your coworker will keep the code running - ►
Exploration: is when you don't exactly know how the code would work, but you are pretty confident on the result, here is where TDD feels like magic - ►
Unit test: to prove a non business logic works as expected, e.g. a duplicate string filter is doing what the method is claiming. - ►
Bug Fix: is a simple technique of simulating a production bug in a test to be sure it will never happen again
So from those types I used to only like the bugfix one, since it could give me a good orientation where the bug might be. Although, to do that, I'd had to set up the entire system, a-lot of work for a simple bug, I was losing too much time to feel safe that this bug would not come back.
> If you want to destroy your credibility, fix the same bug several times. Same outage over and over as an express ticket to the layoff station
So one day I wonder... What if all the tests had that safety feeling? no mocking all data is REAL, only tests that generate value I want to take that test pyramid and burn it 🔥
How Do You Know the Value of a Test
My general rule for a test to have value is that I can trust it. If one test breaks in a pipeline I'm sure It's not a fluke, It tells me something is wrong and I trust it. This is pretty hard to achieve, but having a suit of tests that are not working against you really pays off. In general, your tests should:
- ► Bring you peace of spirit instead of stress wondering if that will not fail.
- ► Tell you were the problem is instead of delaying your delivery.
- ► Be fast and easily reproducible in your local machine
- ► Stay Isolated and not rely on any other test, If you move the code, the test should keep working
- ► Be code agnostic, when you change the implementation without modifying the results, the tests should not need to be changed too
No Mocking
During my research on why I hate TDD so much I stumbled into this book: Test Driven Development: By Example, and one thing struck me really hard, he was not mocking!
And that changed everything, my main source of pain was exactly that. I was guessing results of queries, services, and external implementations; on paper my code was simply perfect, in reality not so much, exactly what was making me doubt testing in the first place
The problem was that most of the time I had no idea what was coming out of the mock as error. Basically the test coverage and all was perfect, expecting an empty array to happen when in production it was coming as sql:NoRowsErr

This was the first thing I changed, mocking as little as possible, with real data and the same environment as in production for the pipeline. These days this is easily achievable with Docker, the only issue is when you have an extensive use of microservices. A test that adds value, here is where the shift starts! You do not need to be guessing if it will work, you are sure that it will work.
You might be wondering how I would make progress when a single test requires 10 inserts and 20 selects to validate the result. How big is this test? Well, let me show
1test.SimpleInternalTest(t, func(r *tester.Runner) {
2 t.Run("description of scenario", func(t *testing.T) {
3 test.InsertLegalText(t, test.InsertLegalTextParams{
4 Id: 265874,
5 Country: "br",
6 TextId: "termsOfUse",
7 AppName: "audiApp",
8 MainLanguage: "pt-BR",
9 })
10 test.CreateLegalDocsMapping(t, []test.CreateLegalDocsMappingType{
11 {Owner: "owner-name", Flow: "name"},
12 }...)
13
14 response := problem.Details{}
15 res := r.Delete("/cms/legal-texts/265874", nil, &response)
16 assert.Equal(t, http.StatusBadRequest, res.StatusCode)
17
18 assert.Contains(t, response.Detail, "some error message")
19 })
20})
This test creates a document, maps it to a login flow and tries to delete it; since we have a flow attached to it, it will not allow. This test is storing data to DB and making a real API call, working not only as an insurance policy but as simple documentation! This is a breakdown of the code:
- ► SimpleInternalTest will generate an entire application and connect to a test database
- ► InsertLegalText will create a document with received parameters and the ones I'm not passing It's gonna use the default ones
- ► r.Delete is a custom HTTP client that is gonna skip authentication code since the goal here is to check business logic and not authorization
Create this helper code was a-lot of work? yes, did it pay off? absolutelly. I've spent some time building it for sure but all the next tests were as simple as this one. The only point you need to be careful is with performance.
This entire process applies perfectly for vibe coding. You can generate readable tests that prove what you want as an output and let the AI work the rest, keeping you on control how on it works while preventing it from breaking running code during hallucinations. Besides, you can use AI to write the test setup too.
Unit Testing Structure
The way I'm structuring code is another interesting way I'm getting value out of my tests. I'm creating a division between whats business logic and what is needed to run my project. Most of the time we tend to combine them into one thing, server configuration and all our code mix with business logic. Testing not only If we can encrypt a link but if that link is the one we have stored in the database makes the entire process way harder than it hast to be.
- services
- - random_service
- - encryped_link_service
- pkg
- - http_client
- - encryption
Here the encryption is wrapping a library with my customizations and configurations, letting my business logic of how to get that data and what data convert to a link independent of how I'm using the library.
This is the default golang structure, where everything under pkg is just shareable code, and they are pretty easy to test, you know exactly what to expect as an output. This is a great way of not letting external dependencies to own your code, wrapping libraries allow you to easily swap them, and mitigate how much access to your code the dependencies have. Removing blind spots on how a dependency works and giving you control over its usage.
For me the hardest part in TDD is testing business logic. TDD is always taught with predefined environments and code, like those in the pkg folder. Testing if 1+1 = 2 is simple, we all know that in real life 1+1=3. Any of those snake-oil salesman trying to sell you the beauty of TDD will only show tests that will never see the light of production in their life.
Conclusion
I've spent years writing tests to conform to companies' rules. Seeing people selling It as "quality"; I could not stand those people saying their code was better than mine just because of the test coverage. The action of writing a test had more importance then solving a single problem. There was no value for me, It was all nonsense propaganda.
This drove me away from TDD the same way the new generation is doing; I see a lot of new developers saying it only slows them down when they need speed the most. I get it, It's true. Spending twice the amount of time you did for a test just to know that foo == foo is crazy! That's the point of this entire article, tests should serve you, not the other way around. You are in control and only YOU know what's best for the project that you are building, make tests add value to you instead of being lectured that It's mandatory.
Here is another take of this problem that I liked:
Law of Demeter violation: violation happens when you have a "train wreck" of method calls, making an object talk to a stranger instead of its close friend. 🚂💥.
order.getCustomer().getWallet().getBalance();
Hard to mock: Insufficient Abstraction / Dependency inversion violation
Hidden Effects: Insufficient separation of concerns
Hidden inputs: Over-encapsulation
I had a code that was returning the user available ownership to a given legal entity. The entire process to get user-id was hidden, and to find out where that value were getting from was hard to find, functional programming would have prevented that from happening
Unwieldy parameter list: too many responsibilities and tests makes us the first user to feel the pain of having too many parameters.
Wishing to have access to private method: If it look like a single responsibility logic, extract it.
"Testing isn't hard. Testing is easy in the Presence of Good Design"