This repository has been archived on 2026-07-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kitchen-md/.claude/adr/0009-testing-tiers-and-boundaries.md
alexion e16f3f957d docs: record testing tiers (ADR 0009), link from 0008 (task 0003)
Add ADR 0009 defining the unit / integration / e2e tiers by the seam
each exercises, with the value-returning vs process-side-effecting line
as the boundary between in-process and e2e-only. Add one line to ADR
0008 noting the Result seam is what makes error propagation assertable
in-process, cross-linking 0009.
2026-07-28 22:24:42 -04:00

2.8 KiB

ADR 0009 — Testing Tiers and Boundaries

Status: Accepted

Context

The test suite accumulated overlapping files — unit, integration, smoke, and subprocess — with no crisp definition of what each was responsible for. The result was duplication and confusion: the same behaviour asserted in more than one tier, and no rule for where a given test belonged. Both specs already referred to unit, integration, and smoke tests, but none of them pinned the boundaries.

Decision

Recognise three test tiers, each defined by the seam it exercises.

Unit — one component in isolation, a pure function, asserted by its return value. parse in core and render in bin are the unit seams.

Integration — several components composed in-process, asserted by the value the composed function returns. The view command function — file read, then parse, then render, returning a Result — is the integration seam.

E2E — the binary as a black box. It is spawned as a subprocess and asserted on its exit code and its stdout and stderr, with no knowledge of the internal structure.

The line between what is unit- or integration-testable and what is e2e-only is whether a function returns a value or performs a process-level side effect. A function that returns a value can be asserted in-process. A function that calls process.exit or process.stdout.write can only be observed by spawning the binary. So all logic is pushed into value-returning functions, and the entry shell is kept as thin as possible, because it is the one part reachable only through a subprocess.

Core exposes a single public seam, parse, so it has unit tests only. A whole-fixture parse test is still a unit test on that same seam with a broad input — a corpus test — not a separate tier.

Rationale

Precise, non-overlapping definitions prevent the duplication that a vague unit-integration-smoke split produced. Classifying by seam matches what is actually cheap or expensive to test. Value-returning code runs fast and is visible to coverage in-process, while process-side-effecting code needs a subprocess and is invisible to coverage. Concentrating the process-boundary surface in one thin shell keeps the amount of e2e-only code to a minimum.

Consequences

Each behaviour is tested at exactly one tier: logic at unit or integration in-process, the process boundary at e2e. The e2e tier is deliberately minimal — it verifies wiring such as exit codes and stream routing, not content already proven in-process. Tests are co-located as {module}_test.ts, so the command-function tests live beside the entry module and are integration tests despite the file name. When a command function shares a file with the top-level program.parse(), that call is guarded with import.meta.main, so importing the module for a test does not run the CLI.