How I fixed TDD and how you can too

Published on February 11, 2026

Reading time: 11 minutes

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:

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 🔥

TDD pyramid

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:

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

bro-meme.png

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:

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"

RELATED ARTICLES

hello word! My first post here

Published on January 1, 2026

This is my first post about finally killing the overengineered blog dream and building something simple as hell: Cloudflare for hosting, Go with templ for HTML, Tailwind for CSS, and a dumb but effective bash script to turn markdown into static pages in a minute. No bloated generators, no AWS circus, no SEO-punished JS nonsense—just write, build, ship 💥. The design leans into Neobrutalism, inspired by post-war brutalist architecture: raw, unapologetic, and not here to look pretty 🤘.

READ MORE →

🏢 Videos on code Architecture

Published on February 6, 2021

Videos I'm watching or I want to watch on the subject of code architecture.

READ MORE →