Overcoming the Fear of Change with TDD
There’s a specific kind of anxiety that doesn’t come from building something new. It comes from touching something that already works.
Overcoming the Fear of Change with TDD
There’s a specific kind of anxiety that doesn’t come from building something new. It comes from touching something that already works.
It Started Simple
I wanted to build one thing that sounded so simple: “let users download their invoice as a PDF. Click a button, file downloads, done.”
It didn’t stay simple for long.
Once the feature started going live, questions piled on fast: “If the PDF already exists in storage, should we reuse it?”, “If the old file is corrupted, should we regenerate it?”, “If the server returns a strange filename, what happens?”, “If HTML rendering fails, does the user still get a fallback?”, “If the PDF template changes, are old files still valid?”, “If the download button is pressed multiple times in quick succession, does the UI hold up?”
At that point, I realized the problem had shifted. It was no longer “can we download a PDF?” The real question was: “can I keep changing this feature without accidentally breaking it myself?”
That’s when TDD started to feel necessary — not as a rule I was supposed to follow, but as something I actually needed.

Illustration by Me!
Before TDD, I was Shipping on Vibes
Honestly? In earlier work, I’d get a feature running, click through it a few times, and call it done. Manual testing felt sufficient because I was the only one who knew all the moving parts.
The problem is that I forget things. The feature doesn’t.
With TDD, before the PDF download was even wired up, I had already committed to paper what it was supposed to do across every scenario I could think of.
The Real Problem
The feature didn’t stop after the first working build. It went through a renderer migration from wkhtmltopdf to Playwright, an invoice template redesign for better branding, HTML normalization hardening, and several rounds of edge-case fixes discovered in production.
By the time development stabilized, the PDF module had accumulated 27 backend test cases and 14 frontend interaction tests .
Without that coverage, each of those changes would have required full manual re-verification. With it, the same validation that used to take 10–15 minutes collapsed into a single automated run.
One regression made the cost of not having tests clear. When the invoice template was redesigned, cached PDFs generated by the old renderer were still being served to users. The test suite caught it immediately, the expected output metadata no longer matched what the regenerated file produced. Without that check, it would have reached production silently, and we’d have needed a hotfix under pressure.

https://firminiq.com/test-driven-development-tdd-and-its-impact-on-quality-assurance-qa/
The Tooling That Made TDD Sustainable
TDD doesn’t survive on discipline alone. If the feedback loop is slow or painful, shortcuts win. What made this stick was a toolchain with clear purpose at each layer.
- pytest (backend unit testing) made it practical to simulate renderer failures, storage corruption, and header parsing edge cases repeatedly without needing real infrastructure. That reduced test setup overhead dramatically — scenarios that would have required a running stack to test manually could be exercised in isolation, in milliseconds.
- vitest + Testing Library (frontend behavior testing) let me test hook logic and page behavior without an actual backend or browser download. Critically, it verified state transitions — not just that the download eventually worked, but that loading → success → error → reset happened in the right order, and that the UI didn't get stuck between states.
- Mocks and stubs isolated the PDF pipeline from external dependencies — storage, the renderer, the database. This made edge-case scenarios reproducible. Testing “what happens if storage returns empty bytes” is nearly impossible in a real environment without corrupting actual data. With mocks, it’s a one-line test setup.
- Logging assertions went beyond checking return values. They verified that fallback paths actually executed. This distinction matters: a swallowed exception looks like a passing test but hides a failure in production.
- Pre-push hooks removed testing from the category of “things developers can forget.” Tests ran automatically before any code entered the repository, making verification a structural part of the workflow rather than a voluntary step.
- Linting and static analysis covered what tests couldn’t: type mismatches, unused imports, inconsistent patterns. They worked as a layer below the test suite, catching problems before tests even ran.
What TDD Actually Gave Me
People say TDD reduces bugs. True, but that undersells it.
What TDD gave me was the confidence to change something that was already working.
Software doesn’t ship once and freeze. It gets hotfixed, redesigned, migrated, hardened, extended. If every change feels like walking into a room with the lights off, that weight accumulates.
Tests remember what mattered. They protect the behaviors you already got right. They let you move faster later — which is exactly when speed matters most.
메타데이터
- post_id
- 7d3e4fa3f156
- slug
- overcoming-the-fear-of-change-with-tdd-7d3e4fa3f156
- url
- https://medium.com/@darwinisfish/overcoming-the-fear-of-change-with-tdd-7d3e4fa3f156
- canonical_url
- https://medium.com/@darwinisfish/overcoming-the-fear-of-change-with-tdd-7d3e4fa3f156
- author_url
- https://medium.com/@darwinisfish
- status
- ok
- fetched_at
- 2026-07-25 17:01:00