Knowledge

AI Writes the Code Now — Test Automation Matters More Than Ever

AI Writes the Code Now — Test Automation Matters More Than Ever

Cover: Test Automation as a Regression Gate

AI writes a feature, you’re happy. Two weeks later another task edits shared logic and the old feature silently breaks — you only find out from a user bug report. That’s regression: the defect isn’t in the writing, it’s in the changing. Code Review is a point-in-time gate; to guard the future you need a gate that keeps running.

First, a correction: AI makes testing matter more

Test automation was already one of the most important regression defenses long before AI-assisted coding. With AI, a common reaction is the opposite: “I don’t write the code anymore, so why should I worry about tests? The AI writes them.” That conclusion is backwards. Three structural shifts point the other way:

Shift Consequence Effect on test automation
You no longer write code line by line You used to hold an implicit dependency map in your head — which call sites a change would touch. Now the AI hands you a large diff and you have to re-read it just to learn which shared functions it touched. Your control drops; an automated gate is the cheapest way to get it back
AI changes are bigger and more confident One task can span a dozen files, and the model never gets the human “I shouldn’t touch this” nervousness. The regression surface grows; manual review can’t cover it
AI is very good at making tests green It will mock out the unit under test, copy the implementation into the assertion, or write smoke tests that assert nothing — then report “all passing.” A green suite becomes deceptive; without a guideline you buy false confidence

One line: AI drove the cost of writing tests down, and the risk of not having them up.

Approach: a 3-layer regression gate

Treat “add tests for this task” as a first-class quality action and front-load it at the same trigger point as review — after the feature builds and lints, before “done.”

Approach Pros Cons Fits
No tests, review only zero extra cost catches only this change one-off, very low-risk
e2e only closest to user slow, brittle, expensive a few critical business flows
3 layers (unit + integration + tiny e2e) complementary coverage, controllable cost needs clear layer boundaries evolving features/modules

We pick three layers, with e2e kept extremely restrained: unit and integration form the everyday “always-running” body; e2e only covers the 1–3 paths users cannot lose.

The key move: humans plan, AI executes

Split “adding tests” into two layers:

So the leverage is: humans freeze the plan into a written guideline; AI executes against it. The guideline buys two things — comprehensiveness and controllability.

Comprehensiveness = a coverage checklist, not AI improvisation

Don’t say “add some tests for this function.” Say “walk these 7 items; if one doesn’t apply, say why in the PR”:

  1. Normal input → an explicit expected value (not “it runs”)
  2. Boundary input: empty / 0 / 1 / max / out-of-range / very large & very small
  3. Illegal input → assert the specific exception type and message
  4. Failure paths: dependency timeout, partial failure, retries exhausted
  5. Idempotency: repeated calls produce the same result
  6. No state leakage: tests are independent, order-agnostic, parallel-safe
  7. Contract stability: field names, ordering, error codes, message formats

Controllability = the test must be able to fail

A test that cannot fail isn’t a guarantee, it’s decoration. AI is especially good at producing those, so hold the line:

The guideline: a TESTING_GUIDELINE.md you can drop in your repo

# TESTING_GUIDELINE.md (read before adding tests — humans and AI both)

## Layer quotas
- unit 70% / integration 20% / e2e 10% (by case count, not by importance)
- e2e covers only the 1–3 "users absolutely cannot lose" paths; justify any addition

## Coverage checklist (answer each per unit; note any skip in the PR)
1. Normal input → explicit expected value
2. Boundaries: empty / 0 / 1 / max / out-of-range / very large & very small
3. Illegal input → assert specific exception type and message
4. Failure paths: timeout, partial failure, retries exhausted
5. Idempotent: repeated calls give the same result
6. No state leakage: independent, order-agnostic, parallel-safe
7. Contract stability: field names / order / error codes / message formats

## Validity red lines (violation = PR rejected)
- ❌ No assertions / bare assert True / print-only
- ❌ Mocking the unit under test (external deps only)
- ❌ Assertions copied from the implementation (tautology)
- ❌ sleep(seconds) to wait for async
- ✅ Mutation sanity check before delivery: break the impl → test must go red

Code: assertion-style unit tests cover boundaries

import pytest

def price_after_discount(base: float, pct: float) -> float:
    if pct < 0 or pct > 1:
        raise ValueError("pct must be in [0, 1]")
    return round(base * (1 - pct), 2)

def test_price_boundary():
    assert price_after_discount(100, 0.1) == 90.0
    assert price_after_discount(100, 0.0) == 100.0
    assert price_after_discount(100, 1.0) == 0.0
    for bad in (-0.1, 1.1):
        with pytest.raises(ValueError):  # assert the exception is actually raised
            price_after_discount(100, bad)

The last block isn’t “just run it and see” — it explicitly asserts the exception is raised. A test that only runs green without asserting isn’t holding the boundary.

Mutation sanity check: prove the test can fail

  def price_after_discount(base: float, pct: float) -> float:
-     if pct < 0 or pct > 1:
-         raise ValueError("pct must be in [0, 1]")
+     pass                       # mutation: drop the input validation
      return round(base * (1 - pct), 2)

# Expected: test_price_boundary must FAIL.
# If it still passes → the test is fake-green. Rewrite it; don't just revert the impl.

This is where “controllability” becomes real: it turns “I think it’s tested” into “I proved this test catches that error.”

Enforced rules

Layers & flow:

  1. Tests enter the same PR as code — front-load, don’t cram before release.
  2. e2e count restrained — as many as there are critical paths, no more.
  3. Gate green before review — machine first, then human review on blind spots.
  4. Run tests with cheap models / CI (echoes model tiering) — don’t burn premium quota.

Constraints on the AI:

  1. Guideline first, tests second — no TESTING_GUIDELINE.md, no unsupervised test generation.
  2. No assertion-less tests.
  3. Mutation sanity check before delivery — if breaking the impl doesn’t turn it red, it’s fake-green.
  4. Never mock the unit under test — external dependencies only.

Outcome (qualitative)

Reproduction checklist

Conclusion & further reading

Code Review guards this change; tests guard the next one. And once AI writes the code, the gate matters more: plan with a guideline, let AI execute, keep e2e tiny, and prove the tests can fail. Pair it with Independent Code Review front-loading for the full pre-merge quality loop.

Note: this is a methodology write-up with qualitative observations; quantitative metrics (regressions caught pre-merge, suite runtime per PR, test-related credit share, fake-green hit rate from the mutation check) are best measured in your own setup. The TESTING_GUIDELINE.md above is a template to adapt — the quotas and checklist should match your project’s shape.

📖 阅读中文版本

← Back to all articles