tim witter
All articles

Blog

Evaluate the claim you actually make

A green result is persuasive only when the check exercises the behavior I describe. I try to formulate the claim first, then design a test that could prove me wrong.

Include the near miss

A positive example may show that the happy path works, but a nearby invalid input reveals whether the boundary is real. My heuristic is to pair each accepted case with a plausible near miss and the expected rejection. This costs extra test design, yet prevents a broad match from looking precise simply because the test never challenged it.

Designing the near miss forces me to say where the boundary is, not just that one exists. If the accepted and rejected cases differ in several ways at once, I cannot tell which difference mattered. I try to vary one property: the same shape with a character too many, the same value just outside a range.

The near miss also guards against a matcher that is broader than my description. A pattern that accepts everything looks perfect against one positive example. The nearby invalid input is the cheapest way to show that acceptance is selective, and it stays valuable after refactors because it encodes the boundary rather than an instance.

classify.test.tsTypeScript
import { expect, test } from "bun:test";
import { classify } from "./classify";

test("accepts the clear case, rejects the near miss, abstains", () => {
  // Accepted: the input matches the declared shape.
  expect(classify("order-4711")).toBe("order");

  // Near miss: one character outside the declared shape.
  expect(classify("order-4711x")).toBe("unknown");

  // An extra digit must not widen the accepted range either.
  expect(classify("order-47111")).toBe("unknown");

  // Ambiguous: no evidence either way, so no label is assigned.
  expect(classify("")).toBe("abstain");
});

Allow an honest abstention

When evidence is missing or the case is ambiguous, declining to classify it can be a correct outcome. I specify when an answer should be withheld, rather than forcing every example into a success or failure label. The tradeoff is fewer confident answers; the benefit is a report that does not quietly count guesses as validated decisions.

Abstention needs a place in the interface, not only in the discussion. If the only outcomes are success and failure, an uncertain case will be forced into one of them, usually the permissive one, because that keeps the pipeline moving. I define the third state explicitly and decide what the caller should do with it.

There is a cost to this honesty. A report that counts abstentions cannot claim the same coverage as one that labels everything, and downstream code must handle a case it would rather ignore. I accept the cost because a forced answer hides the uncertainty without removing it.

classify.tsTypeScript
export type Decision = "order" | "unknown" | "abstain";

const orderPattern = /^order-[0-9]{4}$/;

export function classify(input: string): Decision {
  if (input.length === 0) {
    return "abstain";
  }
  return orderPattern.test(input) ? "order" : "unknown";
}

Check whether the test is still live

Interfaces and fixtures change. A passing test may exercise an obsolete path or a mock that no longer represents the real caller. I inspect what the assertion observes and, where practical, make a small controlled change that should make it fail, then restore it. To verify the final claim, I rerun the focused check on the restored code and state any untested boundary explicitly.

A test can stay green while the behavior it described has moved. The clearest symptom is a test that passes for a reason I did not intend: the fixture supplies the expected value directly, or the assertion checks a copy instead of the original. I read the setup before the expectation, because that is where an unintended pass is usually arranged.

What a failure tells me depends on where it fails. If the test breaks at the assertion, it is connected to the behavior; if it breaks at setup, it may only be connected to the fixture. That is why I watch the failure before restoring the code. A test that failed for the wrong reason proves as little as one that passed for the wrong reason.

break-check.shShell
# Widen the pattern on purpose, then watch the near miss fail.
sed -i 's/{4}/{3,}/' src/classify.ts
bun test src/classify.test.ts
# failed: the near miss is now accepted

# Restore the file and confirm the boundary again.
git restore src/classify.ts
bun test src/classify.test.ts
# passed: the boundary is intact