Index

Changing Existing Software

A Characterization Suite Is Scoped to a Change

A booking endpoint had more than thirty characterization cases. They covered successful bookings, validation failures, wallet rules, locks, pending work, email routing, and several provider response shapes.

The next refactor removed one ambient HTTP dependency from the booking services. The suite looked broad enough.

It was not broad in the direction of the change.

One service read the global request to persist the original payload. Replacing that read with the existing booking data object would keep ordinary responses green while silently storing normalized values where operators expected the raw input. The suite already protected a test-mode short circuit. It needed a new case for the two representations.

Characterization coverage is not a property of a test file. It is a relationship between a proposed change and the behavior that change can disturb.

Start with the dependency being removed

The refactor had a concrete target: booking code below the web boundary should not import an HTTP request or call a global request helper.

Before editing, the retained code exposed three uses of request state:

web request
├── creates normalized booking data
├── selects test mode from a body flag or header
├── passes the request into a booking interface
└── is read globally when diagnostic metadata is stored

Those uses do not have the same contract.

Passing an HTTP object through the booking interface is coupling. Removing it should be invisible to callers. Test mode changes whether remote booking and persistence happen at all. Raw diagnostic input and normalized booking data are both persisted because they answer different questions after a failure.

The first useful artifact is therefore not another test. It is a change map:

Removed dependency useBehavior at riskObservation boundary
test-mode flagsuccess without remote result or retained provisional rowHTTP response and database
raw request readoriginal input remains availablepersisted metadata
request passed to driverdrivers receive required meaning through explicit databooking collaboration and endpoint
request import/helperdependency does not returnsource boundary

The map turns “we have endpoint tests” into four claims which can be checked separately.

Existing coverage gets credit only where it intersects

The endpoint suite already had a useful test-mode case. It sent the body flag, expected success, expected no tracking or document fields, and proved that no shipment with the request reference remained.

That case crossed the right boundary. A unit test of a boolean would not have proved deletion. A source assertion would not have proved the response. The existing endpoint case therefore bought real freedom for moving the flag into explicit booking data while passing the test-mode decision separately.

It did not cover the alternative header which could also enable test mode. The refactor retained that path, but the evidence does not show an equivalent case. That remains a gap rather than becoming coverage by association.

Most of the other characterization cases were valuable for different changes. Wallet rejection tests constrain payment behavior. Email cases constrain notification routing. Provider fixtures constrain response translation. They can catch an unexpected blast radius, but none directly proves how raw input is stored.

A large suite is not one large guarantee. Each test earns a narrower statement.

Raw and normalized data are two different facts

The request boundary normalized country and postal values before constructing booking data. That was correct for downstream processing. Operators still needed the received representation when investigating a surprising booking.

The desired persisted shape was intentionally dual:

received_input
    country: lower-case value supplied by caller
    postal_code: punctuation supplied by caller

booking_data
    country: canonical upper-case value
    postal_code: normalized value used by the workflow

Before the refactor, one field came from the global HTTP request and the other from the booking data object. Removing the global read created an attractive mistake: serialize the booking object into both fields.

The endpoint response would still succeed. The provider would still receive canonical values. Most of the characterization suite would remain green. Only later diagnosis would reveal that the original input had been lost.

The refactor added one focused case. It sent deliberately non-canonical country and postal values, completed a booking, loaded the persisted record, and asserted both representations:

received_input.country != booking_data.country
received_input.postal  != booking_data.postal

That inequality is more useful than checking that both fields exist. It makes the test sensitive to the exact collapse the refactor could introduce.

Move meaning, not the framework object

The implementation kept HTTP interpretation at the web boundary:

HTTP boundary
├── normalized booking data
├── test-mode decision
└── optional raw payload
        |
        v
booking service
├── explicit booking data
├── explicit test-mode value
└── raw payload used only for diagnostics

The booking interface no longer accepted the request object. Drivers received the values they needed through booking data. The service received an optional raw payload from the web entry point rather than reaching back into global state.

The distinction between web and programmatic entry points became visible too. A call built from data has no raw HTTP payload unless its caller supplies one. That is honest. Inventing “raw” input by serializing normalized data would make the diagnostic field lie.

This is a useful rule for ambient-state refactors:

Preserve each required meaning explicitly. Do not replace a hidden dependency with one convenient object which happens to contain most of it.

Sometimes the correct result is several values. A normalized command, actor identity, locale, trace identifier, and raw diagnostic envelope have different lifetimes and trust boundaries even when one request used to provide all of them.

A structural guard closes a different hole

The change also added an architecture test which scans the booking slice for an HTTP request import and calls to the global request helper.

That guard is useful. It makes the new source boundary executable and prevents the same dependency from creeping back into another driver.

It is not proof that booking still works.

The scan cannot establish that:

  • test mode deletes provisional state;
  • raw and normalized values remain distinct;
  • every required request value reached booking data;
  • a provider received the same request;
  • a programmatic entry point has complete context; or
  • a new form of service location was not introduced.

The endpoint and persistence cases prove behavior. The architecture test proves one forbidden dependency shape. Keeping those claims separate prevents a clean dependency graph from being reported as behavioral parity.

Characterization can reveal changes before the refactor

The initial endpoint suite was not a pristine recording of untouched code. While it was being added, the work also corrected small production paths around error formatting and provider response handling.

That matters for provenance. The suite records behavior after those corrections. It cannot prove what every previous deployment did.

This is common when old code first meets serious tests. A test exposes a crash, an impossible fixture, or a branch which cannot produce a coherent response. The team then has two choices:

  1. pin the observed behavior, however awkward, and make the product decision later; or
  2. change the behavior now and record that decision as part of the same work.

Either can be defensible. Calling the second one “characterization only” hides the product change. The commit, brief, and review should say which happened.

The suite should grow in the direction of change

There is no useful target called “complete characterization.” The endpoint in this case crossed validation, payment, persistence, remote calls, documents, email, and recovery. Exhaustively recording every combination would create a slow second implementation of the system’s accidental state space.

Instead, repeat a bounded loop:

  1. state the refactor in one sentence;
  2. inventory every use of the dependency or state being changed;
  3. map each use to a caller-visible, persisted, or operational consequence;
  4. find the existing test which proves that consequence;
  5. add the smallest missing case at the appropriate boundary;
  6. seed the plausible regression and watch the case fail;
  7. make the refactor; and
  8. add a structural rule only for the dependency shape which must stay gone.

The companion synthetic reproduction uses a smaller booking flow. Its broad suite proves ordinary success and a test-mode short circuit. A broken refactor replaces raw diagnostic input with normalized data and the broad suite stays green. One change-scoped persistence assertion fails. A separate source guard rejects ambient request access.

Existing coverage deserves respect. It does not deserve imagination. For the next refactor, ask which exact behavior could change while the current suite remained green. That answer defines the missing characterization work.