I come from a consulting background, having worked with companies similar to TW. Naturally, TDD was drilled into our brains. I’m well-versed with it.
For those unfamiliar, Test-Driven Development (TDD) flips the usual process: instead of writing code first and tests later, you write the test first—watch it fail—then write just enough code to make it pass. Rinse, repeat.
There are some solid advantages:
- It forces you to focus on the input/output contract of functions.
- You tend to write only the necessary code to pass the test—promoting cleaner, leaner implementations.
- You think through edge cases before jumping into implementation.
Sounds clean, right?
But even after trying it seriously, I find myself not loving it. Here's why:
- When clarity is missing, it's hard to TDD. At early stages of dev, I often don’t know the exact input/output—I’m still exploring the shape of the problem.
- It demands iteration. Write a test. Make it pass. Refactor. Repeat. But in real-world scenarios with time pressure and legacy systems, this ideal workflow can feel like a luxury.
- For known problems, where I already know what to build, TDD sometimes feels like a ceremony for ceremony’s sake. When you already have clarity, writing the code first is simply faster and more intuitive.
- Mild OCD here. I hate seeing red tests or compilation errors, even temporarily. It bugs me more than it should, but it does affect my focus.
Recently, in an interview, I mentioned that I prefer writing code first and testing later—and that it's a controversial take. The interviewer laughed, said “Yeah, that’s controversial,” and shut down the conversation without hearing me out.
That rubbed me the wrong way. Not because I wasn’t challenged—I'm open to being wrong—but because it felt dismissive.
So, fellow devs: Am I missing something here? Are these real drawbacks, or am I just bad at sticking to the process?
Would love to hear what other folks think.
Top comments (1)
Hey, one year later, and here’s a response!
I love TDD, although I sometimes skip it for reasons similar to yours. Most of the time, it is because the client does not see the value of TDD or the time it takes to think through the test first.
I’ll go through each point:
How would I use TDD in that case? It depends on the task and the integration. For example, if you are integrating Slack or Stripe, you usually already know the business goal behind it. You can abstract that goal into actions such as “send message,” “process message,” “pay,” or “subscribe.”
Let’s say you know you want to create a subscription and check whether it is valid. You can write a test for that behavior first, then mock the function that talks to Stripe, whether that is an HTTP request or a Stripe SDK call. Once you understand Stripe’s actual schema, you can build fake responses and test how your code behaves under different success and error cases.
This ideal workflow can feel like a luxury.
It can feel that way, but I would say the real luxury is skipping it and relying entirely on QA to catch problems in your code.
When you already have clarity, writing the code first feels faster and more intuitive.
This is actually where TDD shines. If the requirements are clear, you can build your own automated QA safety net that checks your work before deployment.
If the requirements change, the tests should change too. If the requirements did not change but the tests still need to be rewritten, that often means the tests were too coupled to the implementation rather than to the behavior.
If a test is green before you have written the production code meant to satisfy it, you are probably not testing the right behavior.
My main tip for writing good tests is that they should focus on public behavior, not implementation details. In general, we want to test public interfaces instead of private functions that were created only to keep the code clean internally.
If you are testing every class and function only for coverage, then I understand your frustration. In that case, it may be better to write fewer tests and make them more meaningful.
Here is a very oversimplified example just to illustrate the idea. Real codebases are messier, and even the “bad” example below can still be valid depending on the context and what exactly you are trying to verify.
In this simple example, the second test is easier to preserve during refactoring because it checks the public interface instead of internal state. For example, if I later decide to store todo items in a database, the first test may need to change because it depends on how the data is stored internally. The second test may only need a small adjustment, such as using
async/await.That said, the first test is not automatically wrong. There are cases where checking state directly is perfectly reasonable. The point is not that one style is always bad and the other is always good, but that tests tied too closely to implementation details usually become more fragile over time.