Your integration tests check that the answer is correct. Trace-based testing checks that the system got there the right way — before a subtle regression reaches production.

A test that only checks the response body will happily pass while your service quietly starts making three database calls where it used to make one. The output is right; the path is wrong. Traces see what assertions miss.

Assertions on shape, not just output

Trace-based testing runs your normal test, then asserts on the trace it produced. Did the request touch the cache? Did it fan out to more services than expected? Did a span appear that should not be on this path? These are correctness questions your response assertions cannot ask.

What it catches

  • N+1 regressions — a loop that quietly started issuing one query per item.
  • Accidental dependencies — a refactor that made the login path call the billing service.
  • Silent retries — a change that turned one attempt into three, hidden behind a still-correct result.

Assert on the trace in CI

trace_of(test "checkout succeeds")
| assert count(span where db.system == "postgres") <= 2
| assert not exists(span where service == "billing")
| assert max(duration) < 500ms

Where it fits

Trace-based tests are not a replacement for unit tests; they are a complement that guards architecture. We reserve them for the handful of critical paths where "how" matters as much as "what" — checkout, auth, and anything that touches money.