L2 · 10 · AI-Assisted Testing & Code Review

AI writes tests in seconds, but many of them prove nothing. This page teaches you to get strong tests from AI: property-based tests, mutation testing to measure test strength, realistic test data, and a review process where AI catches issues without becoming a rubber stamp.

Level 255 minTracks 5, 12Last verified 2026-10

By the end of this page you can

Why AI tests are often weak

Weak patternExampleAsk instead
Mirror testsRe-implements the function's formula in the assertion"Use hand-computed expected values"
Happy path onlyOne normal input, no edges"Cover empty, boundary, invalid, large and unicode inputs"
Snapshot of current behaviourAsserts whatever the (possibly buggy) code returns"Derive expectations from the spec, not from running the code"
Over-mockingMocks the thing under test or every collaborator"Use real pure functions; fake only I/O boundaries"
Assertion-free"Doesn't throw" is the only check"Every test asserts a concrete outcome"
The best test prompt includes the spec Give the AI the requirements (spec, acceptance criteria, policy) along with the code, and ask it to test the requirements. Tests derived from code alone tend to encode its bugs.

Property-based testing

Instead of a few examples, you state properties that must hold for all inputs, and a library generates hundreds of random cases, shrinking any failure to a minimal example. AI is very good at proposing properties. You judge which are true.

Property typeRetail example
InvariantA discounted price is never negative and never above the original
Round-tripdecode(encode(order)) == order
IdempotenceApplying the same refund twice leaves the balance as after once
Commutativity / order-independenceBasket total doesn't depend on item order
OracleA fast stock calculation equals a slow, obviously-correct one
Here is `applyDiscount(priceCents: Long, percent: Int): Either[String, Long]` and its spec:
percent must be 0..100; result is rounded half-up to the nearest cent; never negative.
Propose 5 properties that should hold for all valid inputs and 2 for invalid inputs.
Then write them with munit-scalacheck. Don't restate the implementation's formula in the properties.
//> using scala 3
//> using test.dep org.scalameta::munit-scalacheck:1.2.0
import munit.ScalaCheckSuite
import org.scalacheck.Prop.forAll
import org.scalacheck.Gen

class DiscountSuite extends ScalaCheckSuite:
  val prices   = Gen.chooseNum(0L, 10000000L)        // up to 100,000.00
  val percents = Gen.chooseNum(0, 100)

  property("result is between 0 and the original price"):
    forAll(prices, percents) { (p, pct) =>
      Discount.apply(p, pct).exists(r => r >= 0 && r <= p)
    }

  property("0% changes nothing, 100% makes it free"):
    forAll(prices) { p =>
      Discount.apply(p, 0) == Right(p) && Discount.apply(p, 100) == Right(0L)
    }

  property("a bigger discount never gives a higher price"):
    forAll(prices, percents, percents) { (p, a, b) =>
      val (lo, hi) = (a min b, a max b)
      (for x <- Discount.apply(p, lo); y <- Discount.apply(p, hi) yield y <= x).getOrElse(false)
    }

  property("invalid percentages are rejected"):
    forAll(prices, Gen.oneOf(Gen.chooseNum(-1000, -1), Gen.chooseNum(101, 1000))) { (p, bad) =>
      Discount.apply(p, bad).isLeft
    }
//> using scala 3
object Discount:
  def apply(priceCents: Long, percent: Int): Either[String, Long] =
    if percent < 0 || percent > 100 then Left(s"percent must be 0..100, got $percent")
    else Right(priceCents - (BigDecimal(priceCents) * percent / 100).setScale(0, BigDecimal.RoundingMode.HALF_UP).toLong)
from decimal import Decimal, ROUND_HALF_UP

import pytest
from hypothesis import given, strategies as st


def apply_discount(price_cents: int, percent: int) -> int:
    if not 0 <= percent <= 100:
        raise ValueError("percent must be 0..100")
    off = (Decimal(price_cents) * percent / 100).quantize(Decimal(1), rounding=ROUND_HALF_UP)
    return price_cents - int(off)


prices = st.integers(min_value=0, max_value=10_000_000)
percents = st.integers(min_value=0, max_value=100)


@given(prices, percents)
def test_bounded(p: int, pct: int) -> None:
    assert 0 <= apply_discount(p, pct) <= p


@given(prices, percents, percents)
def test_monotonic(p: int, a: int, b: int) -> None:
    lo, hi = sorted((a, b))
    assert apply_discount(p, hi) <= apply_discount(p, lo)


@given(prices, st.one_of(st.integers(max_value=-1), st.integers(min_value=101)))
def test_rejects_invalid(p: int, bad: int) -> None:
    with pytest.raises(ValueError):
        apply_discount(p, bad)

Mutation testing: are the tests any good?

Mutation testing makes small deliberate bugs ("mutants") in your code, such as < → <=, + → -, or removing a condition, and runs your tests against each one. If the tests still pass, the mutant survived: you found a gap. The mutation score (killed / total) is a far better quality signal than line coverage.

Your code Mutants< → <=, + → -, … Run tests Tests fail → killed ✓ Tests pass → survived ⚠
LanguageToolTypical use
ScalaStryker4s (sbt plugin)sbt stryker on a module; HTML report of survivors
Pythonmutmut, cosmic-raymutmut run then mutmut results

AI's role: paste the surviving mutants and ask "write the minimal tests that kill these mutants, deriving expectations from the spec". This is one of the highest-value uses of AI in testing. Mutation runs can be slow, so run them per module or nightly.

No tool? Do it by hand Ask the agent: "Introduce 5 plausible bugs into Discount.scala, one at a time, run the tests after each, report which survived, then revert." It is a quick, manual mutation test. Always confirm the code is restored (git diff).

Test data and fixtures

AI-assisted code review

Locally, before you open the PR

  1. Self-review with a fresh reviewer agent (L2 page 08) on git diff main.
  2. Targeted passes instead of one generic review: security, concurrency, error handling, performance, API compatibility. Each is a separate prompt with a checklist.
  3. Fix or consciously dismiss each finding. Note dismissals in the PR description.
Review this diff ONLY for concurrency and resource-safety issues in Cats Effect code:
blocking calls on the compute pool, missing `IO.blocking`, unbounded parallelism (`parTraverse` on
large lists), leaked resources (not using `Resource`), shared mutable state, race conditions on Ref,
missing timeouts on external calls. For each finding: file:line, why it matters, a concrete fix.
If you find nothing, say so. Do not comment on style.

In CI

Keep humans accountable Never let "the AI reviewed it" replace a human approval, especially for security, money, data migrations and public APIs. Automation bias (L1 page 14) is strongest in reviews.
Try it (40 min)
  1. Run the ScalaCheck suite (scala-cli test . with both files). Then break the rounding (use DOWN). Does any property catch it? Add one that does.
  2. Run the Hypothesis tests. Ask AI for one more property and check whether it is actually true.
  3. Do a manual mutation test with your agent on one real class at work-like code. Write tests for the survivors.
  4. Run a targeted concurrency or security review on a recent diff.

Key takeaways