Why Developers Who Skip Tests Always Regret It Eventually

by Arif Ikhsanudin, Backend Developer

It Does Not Hurt Until It Does

The first few months without tests feel fine. Features ship fast. Refactors happen in an afternoon. The codebase is small enough that one developer holds most of it in their head, so the absence of a safety net is not noticeable — because the developer is the safety net.

Then the team grows. Or the codebase crosses some threshold of complexity. Or the original developer leaves. And now every change carries a question nobody can confidently answer: did that break anything?

At this point, the regret sets in. Not in a single moment — it accumulates. Slowly, then all at once.

What the Debt Actually Looks Like

Untested codebases do not fail dramatically. They develop a particular kind of friction that compounds over time.

Refactoring stops happening. A developer identifies a function that is doing three things it should not be doing. They know how to fix it. But fixing it means touching five other files, and there are no tests to tell them if those files still work after the change. So they leave it. This happens dozens of times, across dozens of decisions, and the code slowly calcifies into something nobody wants to touch.

Bug fixes introduce new bugs. Without tests, the only way to verify a fix is to run the application manually and check the specific path you changed. Regression testing — checking that everything you did not touch still works — becomes a manual process that nobody has time to do thoroughly. So regressions ship.

Deployments become events. Instead of a routine part of the workday, a deployment becomes something that requires everyone to be available, monitoring dashboards, ready to roll back. The test suite is supposed to be the thing that makes deployments boring. Without it, they never become boring.

Onboarding slows down. A new developer cannot learn how a function is supposed to behave by reading tests, because there are none. They have to read the implementation, read the callers, and ask someone. That someone has to stop what they are doing to explain. The knowledge is locked in people, not in the codebase.

The Specific Inflection Point

This is not a gradual decline. There is usually a specific point where the lack of tests becomes acute.

For most teams, it happens when the codebase reaches somewhere between 20,000 and 50,000 lines, or when the team grows beyond four or five people. Before that point, coverage-by-familiarity works. After it, you need the tests.

By the time most teams realize this, writing tests retroactively is painful. The code was not designed with testability in mind. Dependencies are hard-wired. Side effects are scattered. Functions accept and return complex objects that require significant setup. Writing a single test can take longer than writing the feature did.

// Code written without testability in mind — hard to test retroactively
public class OrderProcessor {
    public void processOrder(int orderId) {
        // Directly instantiates dependencies — cannot be swapped in tests
        Database db = new Database("prod-connection-string");
        EmailService email = new EmailService();
        PaymentGateway gateway = new PaymentGateway(System.getenv("PAYMENT_KEY"));

        Order order = db.findOrder(orderId);
        gateway.charge(order.getTotal());
        db.markComplete(orderId);
        email.sendConfirmation(order.getCustomerEmail());
    }
}

// Code written with testability in mind — injectable dependencies
public class OrderProcessor {
    private final OrderRepository repository;
    private final PaymentGateway gateway;
    private final EmailService email;

    public OrderProcessor(OrderRepository repository, PaymentGateway gateway, EmailService email) {
        this.repository = repository;
        this.gateway = gateway;
        this.email = email;
    }

    public void processOrder(int orderId) {
        Order order = repository.findOrder(orderId);
        gateway.charge(order.getTotal());
        repository.markComplete(orderId);
        email.sendConfirmation(order.getCustomerEmail());
    }
}

The second version is testable from day one. The first version requires refactoring before you can even write a test. And nobody wants to refactor untested code.

The Way Out

If you are already in this situation, the path forward is incremental. You do not write tests for the entire codebase at once. You write tests for the next bug you fix, the next feature you add, the next function you have to touch anyway.

Specifically: whenever you are about to change a piece of code, write a test that characterizes its current behavior first. Not a test for what it should do — a test that documents what it does right now. This is called a characterization test (the technique comes from Michael Feathers' Working Effectively with Legacy Code), and it gives you a regression baseline before you make any changes.

Over time, the coverage grows organically, in the places that matter most. It is slow. But it is the only approach that works without stopping all feature development to write tests nobody asked for.

The developers who regret skipping tests are not the ones who made a bad decision in a vacuum. They were moving fast with good intentions. The regret comes from discovering that "fast now" and "slow later" were never actually in opposition — they were the same choice, just experienced at different times.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

When Your Client’s “Quick Fix” Becomes a Multi-Day Nightmare

Clients love the idea of a “quick fix.” Reality? It often turns into a multi-day scramble for your team.

Read more

The Hidden Cost of Large Engineering Teams

Big teams look impressive on paper. But behind the scenes, they often move slower, cost more, and create new kinds of problems.

Read more

When a Software Project Goes Wrong: A Contractor’s Perspective

“It was supposed to be done last month… what happened?” From the outside, it looks like failure. From the inside, it’s usually more complicated.

Read more

Planning Your First Year as a Solo Contractor

Starting your own contracting journey can feel exciting and terrifying at the same time. Here’s a roadmap to navigate your first year without losing your sanity—or your savings.

Read more