tim witter
Alle Beiträge

Blog

Die tatsächliche Aussage prüfen

Ein grünes Ergebnis überzeugt mich nur, wenn die Prüfung das beschriebene Verhalten tatsächlich erfasst. Ich formuliere zuerst die Aussage und entwerfe dann einen Test, der sie widerlegen könnte.

Den Beinahe-Treffer einbeziehen

Ein positives Beispiel zeigt vielleicht den Normalfall; eine knapp ungültige Eingabe zeigt, ob die Grenze wirklich hält. Meine Faustregel: Zu jedem akzeptierten Fall gehört ein plausibler Beinahe-Treffer mit erwarteter Ablehnung. Das erfordert mehr Testentwurf, verhindert aber, dass eine zu großzügige Erkennung nur deshalb präzise erscheint, weil niemand sie herausgefordert hat.

Den Beinahe-Treffer zu entwerfen zwingt mich zu sagen, wo die Grenze liegt, nicht nur dass es eine gibt. Unterscheiden sich der akzeptierte und der abgelehnte Fall in mehreren Punkten gleichzeitig, weiß ich nicht, welcher Unterschied zählte. Ich ändere möglichst eine Eigenschaft: dieselbe Form mit einem Zeichen zu viel, derselbe Wert knapp außerhalb eines Bereichs.

Der Beinahe-Treffer schützt außerdem vor einer Erkennung, die großzügiger ist als meine Beschreibung. Ein Muster, das alles akzeptiert, wirkt an einem positiven Beispiel perfekt. Die knapp ungültige Eingabe zeigt am günstigsten, dass die Annahme auswählt, und sie bleibt nach Umgestaltungen wertvoll, weil sie die Grenze festhält.

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");
});

Enthaltung zulassen

Fehlen Belege oder bleibt ein Fall mehrdeutig, kann eine nicht abgegebene Einordnung richtig sein. Ich lege fest, wann eine Antwort ausbleiben soll, statt jedes Beispiel in Erfolg oder Fehler zu pressen. Der Preis sind weniger selbstsichere Antworten; der Gewinn ist ein Bericht, der Vermutungen nicht stillschweigend als geprüfte Entscheidungen zählt.

Enthaltung braucht einen Platz in der Schnittstelle, nicht nur in der Erörterung. Gibt es nur Erfolg und Fehler, wird ein unsicherer Fall in einen der beiden gedrängt, meist in den großzügigen, weil der den Ablauf am Laufen hält. Ich definiere den dritten Zustand ausdrücklich und lege fest, was die aufrufende Stelle damit tun soll.

Diese Ehrlichkeit hat einen Preis. Ein Bericht, der Enthaltungen zählt, kann nicht dieselbe Abdeckung behaupten wie einer, der alles einordnet, und nachgelagerter Code muss einen Fall behandeln, den er lieber übergeht. Ich nehme den Preis in Kauf, weil eine erzwungene Antwort die Unsicherheit verdeckt, statt sie zu beseitigen.

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";
}

Die Aktualität des Tests prüfen

Schnittstellen und Testdaten ändern sich. Ein bestehender Test kann einen überholten Pfad oder einen Stellvertreter prüfen, der den echten Aufruf nicht mehr abbildet. Ich sehe nach, was die Aussage tatsächlich beobachtet, und prüfe nach Möglichkeit mit einer kleinen vorübergehenden Änderung, ob sie fehlschlägt. Nach dem Zurücksetzen starte ich den gezielten Test erneut und benenne ungeprüfte Grenzen.

Ein Test kann grün bleiben, während sich das beschriebene Verhalten verschoben hat. Das deutlichste Zeichen ist ein Test, der aus einem Grund besteht, den ich nicht beabsichtigt habe: Die Testdaten liefern den erwarteten Wert direkt, oder die Aussage prüft eine Kopie statt des Originals. Ich lese den Aufbau vor der Erwartung, denn dort entsteht ein unbeabsichtigter Erfolg meist.

Was ein Fehlschlag mir sagt, hängt davon ab, wo er auftritt. Bricht der Test an der Aussage ab, ist er mit dem Verhalten verbunden; bricht er schon im Aufbau ab, hängt er vielleicht nur an den Testdaten. Deshalb sehe ich mir den Fehlschlag an, bevor ich den Code zurücksetze. Ein Test, der aus dem falschen Grund fehlschlug, belegt ebenso wenig wie einer, der aus dem falschen Grund bestand.

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