Skip to content

Blog · June 8, 2026 · 5 min read · by Codritium Editorial

How to Write Tests That AI Can Maintain

AI is great at writing tests, terrible at maintaining them. The structural choices that make tests survive a year of AI-assisted iteration.

testing
ai-pair-programming
how-to

A model can produce a passing test in seconds. The same model, asked to update that test a month later when the code drifts, will quietly delete an assertion to make the build green. We have watched this happen across 2,840 fix sessions on the platform. The pattern is consistent enough to be predictable. The fix is not to write fewer AI-generated tests. The fix is to write tests with a shape the model cannot wreck without noticing.

Why this matters

A test suite is a slow-decaying asset. Every code change is also a test change, and most teams now route those changes through an AI pair. The model does fine on first authorship. It does poorly on maintenance because it cannot tell the difference between a test that failed because the code is wrong and a test that failed because the test was wrong. When in doubt, it adjusts the test. After a year of this, the suite still has the same number of tests but encodes a smaller share of the actual contract. The team thinks coverage is steady. It is not.

What's the difference between AI-friendly and AI-hostile tests?

AI-friendly tests are explicit about what they protect and silent about how the implementation gets there. AI-hostile tests are the opposite — they encode the current shape of the code in detail and leave the intent implicit. When the code shifts, the AI-hostile test fails for shape reasons, and the model rewrites it to match the new shape. The intent is gone.

The funnel below shows how confidence flows through a typical test pyramid in 2026. Most teams have many unit tests at the bottom and a handful of end-to-end tests at the top. AI helps most at the layers where the contract is local and explicit, and helps least where the contract is implicit in the system's wiring.

Unit1840 testsIntegration410 tests · 22.3% keptContract96 tests · 23.4% keptEnd-to-end24 tests · 25.0% kept
Test pyramid for a representative service in our 2026 cohort. AI authorship and AI maintenance succeed most at the unit layer and fall off as the contract becomes implicit.Codritium repo audit, n=38 services, 2026.

The fall-off is not about the number of tests. It is about whether the test names the contract or merely exercises it. A unit test for calculateTax can say in its name "applies tiered rate above threshold." The model reads that and protects the behavior. A contract test that depends on an external service responding within 200ms encodes that contract in fixture setup and timeout values scattered across the file. The model reads only the failure, not the intent, and tunes the timeout to make it pass.

Why do snapshot tests rot faster with AI?

Snapshot tests are the worst-shaped tests for AI maintenance and the most common kind of test AI is asked to maintain. A snapshot says "the output should be exactly this string." When the output changes, the test fails. A human pauses to ask whether the change was intended. The model defaults to regenerating the snapshot. Three months of this and the snapshot encodes whatever the code happens to produce today.

The bar chart below shows the five most common AI test-maintenance failure modes we observed across a 12-month window. Snapshot rot leads. Silent assertion loss — the model removes an expect call to make the test pass — is close behind.

Share of AI test-maintenance failures by mode, across 4,120 AI-driven test edits.Codritium fix-session log, 2025-Q4 to 2026-Q2.

The escape hatch is simple. Pin the intent, not the output. Most snapshots can be replaced with two or three assertions that name the parts of the output that matter. A test that asserts "the result includes the tenant id and the audit timestamp is within the last second" survives a UI text change. A test that snapshots the entire response payload does not.

When should you mock — and when should you not?

Mocks have a worse reputation than they deserve, but only because most mocks are written at the wrong layer. A mock at a stable system boundary — your database client, your HTTP client — encodes a contract the model can read. A mock of your own code inside the same module encodes implementation, and the model will happily adjust it when the implementation changes. The internal mock is now a fiction the test believes and the code does not.

The table below maps test types to AI authoring and AI maintenance scores, with a recommendation. Green, yellow, and red are coarse, but they match what we observe.

Test typeAI authoringAI maintenanceRecommendation
Pure-function unitGreenGreenLet AI write and maintain freely. The contract is small and local.
Intent-named unitGreenGreenComment-pin the invariant. Then AI maintenance is safe across refactors.
SnapshotYellowRedReplace with two or three named assertions. Snapshots rot fastest under AI.
Mock-heavy unit (internal)YellowRedPush mocks to the boundary. Internal mocks let AI rewrite contracts you didn't agree to.
Integration (real DB)GreenYellowCheap to author, costly when AI silently relaxes setup. Lock the fixture schema.
Contract / consumer-drivenYellowRedAuthor with AI, gate maintenance behind a human owner. The contract is the product.
End-to-end / browserYellowYellowKeep the assertions thin and the setup thick. Flake fixes belong to a human.
Test type by AI authoring and AI maintenance suitability. Green = safe; yellow = supervise; red = do not delegate without review.Codritium platform guide for AI-maintained suites, 2026.

A short before/after makes the snapshot point concrete. Both tests pass the same code. Only one survives a renamed field.

// Brittle: AI will regenerate this on the first failure.
test("renders user card", () => {
  expect(renderUserCard(user)).toMatchSnapshot();
});

// AI-maintainable: pins intent, not shape.
test("user card shows display name and a fresh audit timestamp", () => {
  const html = renderUserCard(user);
  expect(html).toContain(user.displayName);
  expect(html).toMatch(/data-audit-ms="\d+"/);
  // INVARIANT: the audit timestamp must come from the server clock,
  // never from Date.now() in the render path.
});

How do you protect a test from AI rewriting it incorrectly?

Three small habits do most of the work. First, name the invariant in the test name. "Returns the same id for the same tenant across retries" is harder to rewrite badly than "test 42." Second, leave a one-line comment at the top of the test that says what would have to be true in the world for this test to be wrong. The model reads comments. Third, put the load-bearing assertion last and prefer specific matchers over general ones — toBe(3) over toBeGreaterThan(0). Specific matchers tell the model what number you actually meant.

You can also tag tests the model should not silently rewrite. A // @ai-stable comment at the top of the file is a convention the team agrees to, and most pair-programming agents will respect it as long as it appears in the prompt context. It is not enforcement. It is a tripwire — a place where the model is more likely to surface the change than swallow it.

Common questions

  • Should I let AI write all my unit tests?

    Yes for pure-function and intent-named units, with one habit: review the test names before merging. The model writes the test body well; it writes the test name lazily. A bad name today is a bad maintenance signal a year from now.

  • Are mocks worse with AI?

    Only when they mock your own internal code. Boundary mocks — the HTTP client, the database client, the clock — are still fine and arguably more important now, because they encode contracts the model can read. Internal mocks are where the trouble lives.

  • What about end-to-end tests?

    Let AI author them, then take maintenance off the AI's plate. End-to-end tests fail for many reasons and only some of them are real bugs. The model cannot distinguish flake from regression, and its default move — making the test less strict — is the worst possible response to a flake.

  • Do test-comment conventions help AI?

    Yes, more than most teams expect. A `// INVARIANT:` line in a test reduced silent assertion loss by 41% in our internal study. The model reads the comment, treats it as a contract, and resists rewriting the test in a way that violates it. The convention is cheap to adopt and pays back inside a month.

  • How do you stop AI from deleting a test it doesn't understand?

    Make the test small enough that the cost of understanding it is lower than the cost of deleting it. A 12-line test with a named invariant and one assertion is hard to remove without the model surfacing what it's removing. A 60-line test with five overlapping assertions is, from the model's perspective, indistinguishable from dead code.

  • Should we measure mutation score for AI-maintained suites?

    If you can afford it, yes. Mutation testing catches the silent-assertion-loss failure mode directly: a test that no longer detects a real bug is, by definition, a weaker mutation killer. We have seen mutation score drop 8 to 15 points across a year of unmonitored AI maintenance, with line coverage unchanged. Coverage alone hides the regression.

Where to go next