Practical guide

Specify what rejection must leave unchanged

A negative test is useful when it checks how the system handles an unacceptable input or failed operation. “It returned an error” is often only part of that requirement.

Define failure behavior before writing the test

A rejected transfer might need to preserve both balances, avoid sending a notification and leave no successful transaction record. A parser error might need a stable location and no partial update. Identify the observation that would distinguish a safe rejection from a superficially failing operation that already caused harm.

Illustrative example: transfer rejection

Illustrative test inventory
Case Expected response State to inspect
Amount exceeds balance Insufficient-funds rejection Both balances unchanged
Destination is missing Unknown-destination rejection No debit or transfer record
Same request repeats Previously recorded outcome No second debit

The repeated request is not necessarily invalid: the contract might require idempotent success. That distinction belongs in the requirement. Do not classify every unusual input as an expected error and accidentally hide supported behavior.

Arrange the real path to the failure

Supply inputs through the supported entry point and establish the required starting state. If validation normally blocks an input earlier, determine which layer owns the rejection. A unit test that calls an unreachable internal state can help harden a function but does not alone reproduce a public defect. For authorization cases, establish whether the caller can actually control the relevant value.

Make the assertion sensitive to the defect

Illustrative test outline:
  snapshot balances and transaction count
  submit a transfer exceeding the available balance
  assert the specified rejection
  assert balances and transaction count equal the snapshot

Use an independent expected result rather than copying the implementation's calculation into the test. Confirm the assertion fails when the forbidden effect occurs. A test name or requirement annotation is not evidence that this observation was made.

Retain evidence for future changes

Connect the test to its requirement, record the executed revision and keep any minimized reproducer with the resulting defect record. In Proof, scenario or witness annotations describe intended coverage; observed execution evidence is separate. A missing error case may require a requirement amendment as well as a test. Rerun the affected behavior after the fix, and preserve unresolved cases explicitly.

Follow the evidence

Add one assertion about state that must remain unchanged after rejection.

Review one rejection path. Bring the requirement and negative test so we can assess whether it checks both the expected response and the state that must remain unchanged.

Separate invalid input classes · Explore malformed inputs