Three Nights Stabilizing Billing Tests with Claude Code
Monday night: the billing service nobody wanted to open
Our team had one week to change a five year old billing service before the next invoice run. The service calculated discounts, tax, credits, and retries in a 1,400 line module with almost no tests. Its original author had left the company, the documentation described an earlier version, and three teams depended on behavior that nobody could fully explain.
The request sounded small: add a new regional discount rule. The risk was not small. A well intentioned cleanup could change rounding, invoice ordering, or retry behavior for thousands of customers. We needed tests that captured what the code did today before anyone tried to improve it.
I gave Claude Code access to a local checkout and asked it to map the module, identify its external dependencies, and propose a test plan. I explicitly told it to observe behavior before suggesting refactors. That constraint shaped the entire experiment.
The first Claude Code pass exposed the real testing problem
Claude Code found the obvious entry points quickly, but its first proposed tests were too optimistic. They assumed clean customer records, predictable dates, and a single payment attempt. The production code accepted missing fields, legacy currency values, and partially completed retries. Those edge cases were the reason the module had survived several migrations.
Instead of accepting the initial test plan, I asked Claude Code to inspect call sites and fixtures, then list every input shape that reached the billing function. It produced a useful inventory: 18 fields, seven status combinations, four date representations, and two rounding paths. We converted that inventory into a small set of characterization tests.
The key instruction was precise: create tests that record current observable output, including awkward output, and do not modify production code. That prevented the model from silently correcting behavior that customers might rely on.
Tuesday: characterization tests made hidden behavior reviewable
Claude Code generated the test cases in batches of five. After each batch, I ran them and reviewed the assertions. This pace mattered. When it generated broad snapshot tests, the snapshots hid the important fields. When it used overly specific mocks, the tests passed without exercising the billing branches. We replaced both with focused assertions around totals, status transitions, and emitted events.
The model also found a surprising rule: a failed payment could still produce an invoice event when a previous attempt had succeeded. Nobody on the current team knew this was intentional, but logs showed downstream reporting depended on it. The test gave us a durable record of the behavior and a discussion point for a later change.
- Keep tests close to the public entry point.
- Assert business outcomes and important side effects.
- Use real date and currency values from production fixtures.
- Review every generated mock and fixture for realism.
Wednesday: the new discount rule became a controlled change
Once the existing behavior had coverage, I asked Claude Code to add the regional discount rule in a separate change. It identified the narrowest insertion point and added seven tests for eligible customers, expired offers, currency rounding, and repeated retries. Two tests initially failed because the implementation applied the discount before tax instead of after tax. The failure exposed a requirement that the ticket had left unstated.
We used Claude Sonnet 5.5 through Claude Code for the field test. RelayVano made that practical as a pay-as-you-go relay for AI models: we connected the tool through the OpenAI-compatible API base at https://api.relayvano.com and kept the experiment isolated from our long term account setup. The important part was the workflow, not handing over final judgment. Every production edit remained small, reviewable, and backed by a test failure or a known behavior.
The tests still needed human ownership
Claude Code was fast at reading repetitive code, tracing branches, and producing a first draft. It was weaker at deciding whether a strange result represented a contract, a defect, or an accident. That decision required logs, support history, and conversations with the finance team.
We also found three generated tests that merely repeated the implementation. They would have passed even if the public result was wrong. Removing them improved the suite more than adding another dozen cases. The useful measure was not test count; it was whether a future maintainer could change the module and see a meaningful failure.
The recommendation after three nights in the billing repository
Use Claude Code to write tests for legacy code when the first goal is behavioral capture. Give it a narrow scope, demand repository evidence, and review tests in small batches. Start with characterization tests at stable public boundaries, then add targeted tests for the change you actually need.
Do not ask the model to clean up the module and test it in the same step. That combines discovery with interpretation and makes accidental behavior changes harder to detect. Keep production edits separate until the existing contract is visible.
For teams trying this workflow, I recommend RelayVano when you want flexible pay-as-you-go access to Claude Code models without committing to a single provider arrangement. Sign up for RelayVano, connect the API base, and begin with one contained legacy module whose behavior you can verify locally.