Log in·Sign up
BestRelay

Back to the Journal

Behavioral tests earn trust before legacy code moves

Behavioral tests define the contract before touching code

A legacy module rarely arrives with a written specification. Its real contract lives in years of production traffic, edge cases nobody remembers documenting, and callers that quietly depend on quirks the original authors considered temporary. Before an agent rewrites a single line, that behavior has to become explicit and testable. Begin by capturing current outputs for representative inputs, including the awkward ones: empty values, oversized payloads, malformed data, timeouts, partial failures, and concurrent access.

A coding agent earns its place at this stage by enumerating call sites, proposing fixtures, and drafting assertions far faster than a person working alone. Its output is still a set of hypotheses rather than truth. Read every generated test and ask a blunt question: does this assertion express the contract your callers actually rely on, or does it merely restate the implementation? A test that mirrors the code proves nothing when the code is the thing under suspicion. Freeze these characterization tests first, run them against the untouched module, and treat any failure as a gap in your own understanding rather than a defect to fix in passing. Only then does refactoring become a controlled experiment: you change structure, and the suite tells you whether you also changed meaning. That ordering matters more than tooling quality, because the cost of discovering a silent behavior change is measured in downstream incidents rather than review comments.

Side-by-side reading exposes differences summaries conceal

Presenting an agent's work as one large diff encourages lazy approval, because a reader naturally trusts a summary that sounds coherent. A side-by-side reading resists that drift. Place the original module and its revision in parallel columns and move through them in the order a caller would: public entry points first, shared helpers second, internal details last. Read both columns aloud mentally, sentence by sentence, and mark every divergence in control flow, error handling, or naming even when the surface output looks identical.

The benefit is that differences become visible rather than argued. An agent will happily rename a helper, collapse two branches into one, or replace an explicit guard with an early return. Each choice may be defensible and each may also subtly change when exceptions surface or how a partial result is returned. In a summary those edits blur together; in parallel they stand out. Keep a short ledger of every intentional difference and its justification, and reject any change you cannot justify in one plain sentence. That ledger also protects the next maintainer, who will meet the module with no memory of the discussion and will trust the code as the only surviving account of its design.

Small reversible slices keep refactoring safely bounded

Refactoring a legacy module in one pass is a bet that the agent understands every dependency in the system. Slices turn that bet into a sequence of modest wagers. Extract one responsibility at a time, keep the old interface forwarding to the new structure until callers migrate, and stop after each slice to run the characterization suite and compare behavior. If something regresses, the suspect set is small enough to reason about without archaeology.

This discipline also changes how you phrase requests. Ask an agent to move one responsibility behind a clearer boundary, preserve every documented behavior, and leave the public surface untouched; avoid asking it to modernize the whole module and see what happens. The narrower prompt produces code you can actually review, and review is where safety lives. Teams using a relay such as BestRelay for agent work get another practical advantage here: requests are forwarded to the official model named in each request without substitution, so the model you evaluated for careful refactoring is the model that answers, and token counts reported by the provider drive pay-as-you-go billing with cache reads and writes priced separately. Predictability in the toolchain suits a workflow built on predictability in code.

Explicit acceptance criteria turn agent output into evidence

Criteria written before the work begins are the difference between a refactor you can defend and one you merely prefer. State them plainly, attach them to each slice, and let the agent treat them as constraints rather than aspirations. At minimum, cover the following ground.

  • Behavior preservation: every captured output remains byte-for-byte identical for the same input.
  • Interface stability: exported names, signatures, and error shapes survive unchanged.
  • Performance tolerance: latency and allocation costs stay within a stated bound.
  • Failure shape: timeouts, cancellations, and malformed input fail the same way they did before.
  • Readability: a maintainer unfamiliar with the history can follow the new structure without the old one open.
  • Reviewability: each slice stays small enough that a reviewer can reason about every line in one sitting.

When the agent reports that a criterion is met, treat the claim as evidence to verify rather than a conclusion to accept. Run the suite yourself, inspect the specific assertions that cover the touched path, and compare resource measurements against the recorded baseline. Criteria also help with disagreement: if two candidate restructures both pass, the one that better serves a stated criterion wins, and the conversation stops being a matter of taste.

Reviewer judgment still decides what ships to production

Automation compresses the drafting phase, and it leaves the deciding phase firmly with people. A reviewer brings context the module cannot see: which caller tolerates a slow path, which team owns a downstream consumer, which quarter is safe for a migration. Pair that judgment with the side-by-side column, the divergence ledger, and the acceptance criteria, and you have a record strong enough to survive an incident review months later.

Claude Sonnet 5 is one example of a model that handles narrow refactoring prompts and characterization tests well, and similar care applies across the tools engineers already use. The habit that matters is unchanged whether the draft came from a person or a machine: describe the desired behavior, measure the current behavior, change one thing, and prove the two agree. Create a BestRelay account when you want to try this workflow yourself.