DEV Community

장지호
장지호

Posted on

When You Start Writing Tests, What Changes Isn't the Code, It's the Fear

A developer who has never written a test hesitates at the refactor button. They can't be sure what will break. A developer who has tests behind them hits the same button without flinching. What separates the two isn't skill. It's whether they have a way to check their work. Junior or senior, without that check, people tend to build around the code instead of changing it. A few years of that and you end up with a block nobody dares touch.

Refactoring is scary because there's no way to check your work

With tests, you find out within seconds of a change whether anything broke. Without them, you're stuck clicking through screens by hand, or you skip verification entirely and find out from a production alert. Martin Fowler defines refactoring as changing the structure of code without changing its behavior. There's no good way to prove behavior stayed the same except by running tests before and after and watching for green. Once that check gets cheap, you end up touching code more often. Stuff you used to avoid because it was too scary to touch becomes something you can fix, and technical debt piles up more slowly as a result.

The moment you reproduce a bug, that reproduction is already a regression test

If you write reproduction code before fixing a bug, you get a regression test for free. The usual sequence without tests is: reproduce, find the cause, fix, eyeball the result, close the ticket. Months later someone touches nearby code and reintroduces the exact same bug, with no idea it ever happened before. Turn the reproduction into a test and you record the conditions that triggered the bug, which inputs, which state, in a form the computer can run on its own. Break those conditions again and the test tells you immediately. Keep this up and your test suite starts to read like a history of every incident the team has had. Reading test names and comments beats digging through the commit log for what went wrong last time.

Code that's hard to test is usually code that's badly designed

If testing a single function means spinning up a database connection, calling an external API, and wiring up global state, that's a sign the design had problems long before anyone wrote a test for it. Michael Feathers, in his book Working Effectively with Legacy Code, defines legacy code as code without tests. Flip that around and it also means code that's easy to test is usually well designed. Once you start writing tests, injecting dependencies from outside the function becomes a habit. You hide database calls behind an interface. Anything that changes from run to run, like the current time or a random number, gets passed in as an argument instead of generated internally. What you end up with is more pure functions: same input, same output, every time. This isn't extra work done just to satisfy a test. Code that's easy to test tends to be easy to maintain, so the two pull in the same direction on their own.

Make coverage numbers the goal and testing turns into a burden

A hundred percent coverage doesn't mean your code is safe. Martin Fowler wrote, in a blog post titled Test Coverage, that low coverage is a useful warning sign, but high coverage doesn't prove much (martinfowler.com/bliki/TestCoverage.html). Line coverage only checks whether a line ran at least once. It says nothing about whether you checked the result of running it. Call a function and never assert on its return value, and your coverage number still goes up. So when a team sets coverage as a target, you get more tests that run code without checking anything, which is the opposite of what anyone wanted. Testing implementation details is another common trap. Assert on internal variable values or the order private methods get called, and changing the internals breaks a pile of tests even though the visible behavior never changed. Tests like that don't support refactoring. They punish it, and that undermines the whole point of writing tests in the first place: being able to change code without dread.

So where do you actually start

Don't try to retrofit tests onto an entire existing codebase. Add them to whatever you're touching right now, and whatever bug you're fixing right now. Plans to blanket an old codebase in tests tend to fall apart within a few weeks. It's much less overwhelming to test only what you're already working on: a new function, today's bug, next week's module. When you fix a bug, write the reproduction test before the fix itself, and you get the regression protection described earlier for free. Instead of reporting coverage percentages as a management metric, pull a list of files that change often and files that have actually caused outages, and fill in tests there first. That does more to prevent real incidents. I think tests, in the end, change how you relate to your own code. From the moment you start writing them, refactoring gets less scary. Every bug becomes a record instead of a repeat, and the design keeps getting pushed toward something simpler. But it's worth remembering that the moment you start measuring that tool by a number, it turns into a burden.

Thanks for reading.

Top comments (0)