Adding Tests to a Codebase You Did Not Write

SprintX Team

Written By

SprintX Team

AI & Product Engineering

August 10, 2026

6 min read

A test suite being built around an existing untested codebase

You do not need full coverage. You need tests around the paths that lose money or data — here is the order to add them in when you did not write the code.

The codebase works. It also has no tests, and you did not write most of it — an AI agent did, or a contractor who has moved on, or you six months ago in a mode you no longer remember being in. Now every change feels like a coin flip, because there is nothing that will tell you when you break something that used to work.

The usual advice at this point is to write unit tests, and the usual outcome is that you write forty of them for the utility functions that were never going to break, get bored, and stop. Coverage percentage goes up. Confidence does not.

Testing an inherited codebase is a different job than testing one you are building. You are not specifying behavior — the behavior already exists and users depend on it. You are locking in what works so you can change everything else. That reframing changes what you test, in what order, and how much of it you need.

The goal is not coverage, it is permission to change things

Ask what you want to do that you currently cannot. The honest answer is usually: refactor the messy parts, upgrade a dependency, change the data model, or let someone else touch the code. Every one of those is blocked by the same thing — no way to know whether the app still works afterward.

So the target is not eighty percent coverage. It is a suite that runs in under two minutes and fails loudly if a user cannot sign up, log in, pay, or see their own data and nobody else's. That is maybe fifteen to twenty-five tests, and it is genuinely achievable in a week.

Coverage as a metric is worth ignoring at this stage. A codebase can hit seventy percent by exercising every getter and still have nothing checking that a customer's payment gets recorded. Judge the suite by what it would catch, not by what it touches.

Start at the top of the pyramid, not the bottom

Conventional advice says many unit tests, fewer integration tests, a handful of end-to-end tests. That is good advice for code you are writing. It is the wrong starting order for code you inherited, for two reasons.

First, you do not know what the units are supposed to do. Writing a unit test for a function whose intent you are inferring locks in whatever it currently does, bug included, and gives you false confidence.

Second, AI-generated code tends to have poor unit boundaries. Logic lives inside components, route handlers do six things, and functions reach directly into the database. Unit-testing that shape requires refactoring first — which is exactly the thing you cannot safely do yet. If you do not really understand the code you are testing, what to do when you do not understand AI-generated code is the right place to start before writing a line.

So start with end-to-end tests through the real interface. They require no knowledge of internal structure, they survive refactoring completely (which is the entire point), and each one covers a lot of ground. Once those exist, you can restructure the inside freely and add unit tests as the boundaries become clear.

The first five tests

Write these before anything else. In most apps they cover the failures that would actually hurt.

1. Signup through to a usable account. Register a new user, follow the confirmation flow, land in the app, and assert the backing records exist. This one test exercises auth, email, database triggers, and access policies at once — the exact chain behind signup failing while login works.

2. Login and logout. Including that logout genuinely invalidates the session rather than just clearing local state.

3. The core action of your product. Whatever the app is for: create the project, send the message, generate the report. Perform it as a real user and assert the result persisted.

4. Tenant isolation. Create data as user A, log in as user B, assert B cannot see or modify it. This is the test that catches the highest-severity class of bug in AI-built apps, and almost nobody writes it. If your app uses row-level security, it is the only real proof your policies work — see Supabase RLS mistakes in AI-generated apps.

5. Payment, if you take money. Checkout with a test card through to the webhook that grants access. The gap between test mode and live is its own failure mode, covered in Stripe working in test and failing live.

Five tests. Half a day to a day each including setup. When they pass, you can deploy on a Friday.

Characterization tests for the parts nobody understands

For gnarly internal logic — pricing calculations, scoring, date arithmetic — you often cannot tell what the correct answer is supposed to be. Use characterization tests: feed the function a set of realistic inputs, record whatever it currently returns, and assert that it keeps returning that.

You are not asserting correctness. You are asserting "this did not change", which is precisely what you need before refactoring. If one of the recorded values is wrong, you will find out when a user complains, and then you fix the code and the recorded expectation together — deliberately, with a visible diff, rather than silently.

This technique is what makes refactoring AI-generated code survivable. Lock in current behavior, restructure underneath, keep the tests green.

What not to test

Just as important, and the reason most of these efforts stall.

SkipWhy
Presentational component snapshotsThey break on every style change and catch nothing
Third-party library internalsStripe and Supabase test their own code
Trivial getters and pass-throughsNo logic, no possible failure
Framework behaviorRouting and rendering are already tested upstream
Code you plan to delete this monthTesting it makes it harder to delete
Exhaustive validation permutationsTest the validator once, not every field

The exception to the "no snapshot" rule is a component with real conditional logic — a permission-gated view that hides an admin button. Test the logic, not the markup.

Making the AI write them (carefully)

You can absolutely use an AI agent to write these tests, and it is much better at test code than at architecture. Two rules make it work.

Give it the specification, not the implementation. If you point it at the function and say "write tests", it will read the code and assert exactly what the code does, bugs included — a test that can never fail. Instead, describe the intended behavior and let it write tests against that description. Tests that fail immediately are informative: either the description is wrong or the code is.

And review every assertion. Generated tests are fond of asserting that a function was called rather than that anything happened, which passes forever regardless of correctness. If an assertion does not describe a user-visible outcome or a stored fact, rewrite it.

Make the suite matter

A test suite nobody runs is documentation with extra steps. Wire it into CI so it runs on every pull request and blocks merge on failure — that is the whole payoff, and CI/CD for a vibe-coded app covers the setup.

Then adopt one rule going forward: every bug fix gets a test that fails before the fix and passes after. It costs fifteen minutes, it guarantees the bug never returns, and over a year it builds a suite shaped exactly like your app's real failure modes rather than like a coverage report.

Frequently asked questions

How much coverage do I need before I can refactor safely? Coverage is the wrong measure. You need every path that would embarrass you if it broke — auth, payments, data isolation, the core action — covered end to end. In practice that is fifteen to twenty-five tests and often lands somewhere around thirty to forty percent line coverage, which is plenty for the purpose.

Should I write tests or fix bugs first? Write the test that reproduces the bug, then fix it. You get a verified fix and a permanent regression guard for barely more effort than the fix alone, and you build the suite from the places your app has actually proven fragile.

Is it worth testing an app I might rewrite? Yes, more than usual. End-to-end tests written against the interface are independent of implementation, so they carry over to the rewrite and become your definition of done. They are the only artifact from the old codebase guaranteed to still be useful afterward.


If you are afraid to touch a codebase you did not write, the fix is a small suite around the paths that matter rather than months of coverage work. SprintX builds that safety net for inherited and AI-generated apps — the critical-path tests, the CI wiring, and the refactoring it unlocks. Send us your repo.

Related Articles

Contact us

to find out how this model can streamline your business!