Files
tiny-pattern-ts/development/testing.md
T
tmu dc7ce22fc5 📝 Add development/ docs for decisions and known issues
Category files under development/ replace the decision prose that was
scattered through README.md and CONTRIBUTING.md. Each decision is a block with
Decision (YYYY-MM) / Why / Rejected / Known issue; the new library.md records
the public API contract and its limitations. AGENTS.md and backlog.tasks now
point at the new home.
2026-09-15 13:26:59 +00:00

3.4 KiB

Testing

For this library the types are the feature — narrowing, exhaustive() returns, the Matcher<T> contract — so a runtime-only test loop would verify the wrong thing. The contributor-facing commands are in CONTRIBUTING.md; this file records why the loop is shaped the way it is.

Type-driven development

New behavior follows type-driven development (in Edwin Brady's sense): treat the type as the plan for a program, and use the compiler and type checker as your assistant, guiding you to a complete program that satisfies the type (idris-lang.org). Here that plan is the expectTypeOf assertion, written first. The loop is type → red → green → refactor:

  1. Type — write the compile-time expectation first (expectTypeOf(...).toEqualTypeOf<…>()) and let npm run check:tsc fail on the type. The type error is the spec you want to hit before the runtime logic exists.
  2. Red — add the matching runtime assertion (assert.*) so npm run test:unit now fails on behavior.
  3. Green — implement in src/*.ts until both the type check and the test pass.
  4. Refactor — with the type system and the tests as the safety net, then npm run verify as the definition-of-done gate.

This is why every test in the suite pairs an expectTypeOf(...) with an assert.* — keep them together.

Decision (2026-09)

Test-driven development is type-driven here: the compile-time expectation is written before the runtime assertion, and both before the implementation.

Why

  • A runtime-only test can pass while the type is wrong, and a type-level library would then ship a broken feature that its tests bless.
  • The type error is a more precise spec than a failing assertion, because it states the exact expected type before the logic exists.

Rejected

  • Runtime-first (classic red/green): it verifies the value, not the contract, and the contract is the product.
  • Testing the type only: it would not catch handler wiring, exhaustive() throwing, or the otherwise fallback (see src/index.test.ts).

Enforcement

Type-first is also enforced structurally: npm test runs check:tsc before the test runner, so a wrong type can never be papered over by a passing assertion. Per AGENTS.md § Never do, reach green honestly — fix the types so both the type check and the runtime assertion pass, never suppress the ones you can't make pass.

Test tiers

  • npm run test:unit — the test runner alone, for the fast local loop.
  • npm test — check:tsc + the unit suite; this is what pre-push and the baseline check run.
  • npm run test:ci — adds c8 coverage; used by CI. c8 uses V8 coverage, so the --strip-types source is instrumented without a build step.

The runner is node --test --strip-types "src/**/*.test.ts". It relies on the .ts import-extension convention (see tooling.md).

Known issue

  • The type-aware linter misidentifies expectTypeOf() as a floating promise, so src/index.test.ts carries a file-level oxlint-disable typescript/no-floating-promises with an explanatory comment. This is a known false positive, not a rule we want off project-wide (see tooling.md § oxlint-disable directives live next to the code).