mcpField
M
maintainable-tests

maintainable-tests

github.com/jpcaparasTesting & quality5661 installsUpdated 1d ago
Repository

About maintainable-tests

maintainable-tests is a skill for developers writing, refactoring, or reviewing automated tests. It emphasizes behavior-focused examples, clear test intent, deterministic execution, and maintainable test doubles.

Key features

  1. 01Behavior-focused test naming
  2. 02Readable, concrete examples
  3. 03Guidance on fixtures and test doubles
  4. 04Determinism and side-effect isolation
  5. 05Review and verification checklist

Use cases

  1. 01Adding tests for new behavior
  2. 02Writing regression and boundary coverage
  3. 03Reviewing test changes for brittle coupling
  4. 04Testing code with external effects or global state

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/jpcaparas/skills --skill maintainable-tests

After installing, type/maintainable-testsin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

AGENTS.md
metadata.json
README.md
skill-card.prompt.md
agents/
openai.yaml
assets/
.gitkeep
evals/
evals.json
evals/files/
publish-config.spec.ts
references/
doubles-and-boundaries.md
gotchas.md
legacy-and-characterization.md
naming-and-intent.md
principles.md
review-rubric.md
side-effects-and-compatibility.md
source-notes.md
structure-and-fixtures.md
scripts/
analyze_maintainable_tests.py
test_skill.py
validate.py
templates/
test-review.md

Details

From the project README

maintainable-tests

Write, edit, refactor, and review tests so a developer returning months later can understand the behavior, the edge cases, and the reason each important test exists.

Passive Trigger

Load this skill in the background whenever the task involves automated tests, examples used as tests, fixtures, mocks, stubs, fakes, test data builders, regression coverage, characterization tests, flaky tests, or production-code changes made to improve testability. Keep it lightweight for small edits: apply the core rules silently, then mention only the test-design decisions that affect the final implementation.

Decision Tree

Consult the relevant reference only when a non-obvious test-design decision needs more detail than these rules or local examples provide. A matching topic does not require a reference read.

  • Adding tests for new behavior: Name each test after the user-visible rule or domain invariant, then use concrete examples that teach the behavior. Consult references/principles.md or references/naming-and-intent.md if intent is unclear.

  • Fixing a bug or adding regression coverage: Prove the broken scenario and expected behavior, reusing adequate coverage where it exists. Consult references/legacy-and-characterization.md when preserving legacy behavior or explaining uncertainty needs care.

  • Covering edge cases: Keep the happy path visible, then cover meaningful boundaries without duplicating adequate tests. Consult references/structure-and-fixtures.md for difficult fixture or case organization.

  • Tests are hard because the production code is tangled or unmockable: Use existing safe seams first, including framework overrides or scoped, reliably restored legacy patches. Refactor production only when necessary, proportionate, and authorized. Consult references/doubles-and-boundaries.md, or {{ skill:maintainable-code }} if available, when a boundary decision remains unresolved. In tests-only work, propose a needed production change rather than making it.

  • Refactoring legacy code before changing behavior: Reuse adequate behavior coverage; add characterization only for important gaps. Label intentional legacy behavior and change production code in small verified steps. Consult references/legacy-and-characterization.md when the preservation contract is unclear.

  • Reviewing a test diff: Lead with tests that can pass while behavior is broken, brittle implementation coupling, and missing edge coverage before style. Consult references/review-rubric.md for difficult review judgments; its format is optional.

  • Choosing mocks, stubs, fakes, fixtures, or integration tests: Prefer the least powerful test double that proves the behavior and adequate contract or integration coverage where adapters can drift. Consult references/doubles-and-boundaries.md when the choice is non-obvious; existing coverage counts.

  • Tests touch network, waiting, global framework state, environment gates, filesystem/CLI effects, or multiple supported dependency versions: Deny unintended effects, reset global state, test relevant configuration dimensions, and assert the exact artifact or capability branch. Consult references/side-effects-and-compatibility.md for unresolved isolation or compatibility risks.

Quick Reference

SituationDefault action
New domain ruleWrite one readable example for the normal case and focused examples for meaningful boundaries
Test nameState behavior and outcome: rejects withdrawals that exceed the current balance
Test bodyPrefer Arrange / Act / Assert, with setup kept close enough to read as a story
Repeated setupExtract helpers only when the helper name preserves domain meaning
Parameterized testsUse named cases that explain why each row exists
Legacy behaviorAdd a short rationale: compatibility, data migration, customer contract, bug reference, or explicit unknown
Hard-to-test production codeReuse safe seams; refactor only when necessary, proportionate, and within authorized scope
Mock-heavy testReplace incidental interaction assertions with behavior assertions, fakes, or adapter contract tests
Edge caseName the boundary, not just "handles invalid input"
Network or subprocess in testsFail on unplanned calls; explicitly fake, stub, or integrate only the contract under test
Sleep or retry delayReplace real waiting with a fake scheduler/clock and assert the requested delay when it is behavior
Global framework stateRestore a known baseline before each test and clean up afterward when the framework cannot isolate it
Configurable behaviorCover the dimensions in its contract: usually default/override/effect, plus environment or capability branches only when relevant
File or CLI effectAssert exit/result plus the exact path, type, contents, backup, and unchanged-on-cancel behavior that matter
Compatibility promiseExercise the lowest supported combination and current versions; add capability-present/absent paths when production has an optional capability
ReviewAsk whether the test would teach a new maintainer what behavior matters

Core Rules

  1. Treat tests as living documentation. A reader should learn the feature, its vocabulary, and its important boundaries from the test names and examples.
  2. Prefer behavior names over implementation names. Test rejects expired invitations, not returns false from validateInvite.
  3. Keep examples concrete and domain-shaped. Use real values, currencies, roles, states, dates, and IDs when they clarify the rule.
  4. Make intent local. A test should not require reading a distant fixture factory, hidden global setup, or framework magic before the behavior makes sense.
  5. Use DAMP tests when readability and DRY conflict. Duplication that keeps the scenario clear is often better than clever shared setup.
  6. Keep each test focused on one behavior, but assert every outcome needed to prove that behavior. State changes, returned results, and emitted events can belong together when they are one observable rule.
  7. Choose test doubles by contract: stubs answer queries, fakes model simple state, spies observe important effects, mocks enforce essential interactions only.
  8. Do not expose private internals, freeze bad abstractions, or add broad interfaces just to make a test pass. Prefer existing safe seams; reshape a production boundary only when necessary and proportionate, and never exceed a tests-only scope.
  9. Document edge cases and legacy behavior where the name alone cannot carry the reason. The future reader needs to know whether behavior is principled, historical, contractual, or temporary.
  10. Keep tests deterministic. Control time, randomness, external services, locale, timezone, concurrency, and persistence at clear boundaries.
  11. Verify the failure mode, not only the happy path. A regression test should fail for the bug it guards against.
  12. Match the local test framework and style before importing a new pattern.
  13. Make unintended effects fail fast in the test harness. Unstubbed network calls, real sleeps, destructive commands, and shared process state should never pass unnoticed.
  14. Assert the real observable with the right subject and type. A boolean existence check compared with file contents, or a correct assertion against the wrong path, is a false positive.
  15. For configurable behavior, test the contract's decision dimensions separately from the effect. Exercise capability-present and capability-absent paths when production supports an optional capability.

Maintainable Test Gate

Before finishing test changes, run this gate mentally and with local tooling where available:

GatePass condition
IntentThe test name says the behavior and expected outcome in domain language
StoryArrange / Act / Assert or equivalent phases are easy to see
EvidenceAssertions prove observable behavior rather than incidental implementation
FixturesSetup is small, named, and close enough to understand without archaeology
Edge casesBoundary scenarios are explicit and explain why the boundary matters
Legacy contextHistorical or compatibility behavior has a rationale or ticket reference
DeterminismTime, randomness, I/O, network, database, and process state are controlled
IsolationUnplanned network, waits, destructive commands, and leaked global framework state fail or are reset explicitly
CompatibilityLowest/current supported combinations are exercised, plus capability-present/absent branches when an optional capability is part of the promise
Artifact evidenceAssertions inspect the exact path, value type, contents, and unchanged state that prove the effect
DoublesMocks, stubs, fakes, and spies are the least powerful option that proves the rule
Production designSafe existing seams are used; any production refactor is necessary, proportionate, and authorized
HandoffA new maintainer can use the tests as an onboarding map for the behavior

Operating Workflow

Keep small edits small: use focused context and adequate existing coverage, without a written story or fixed report template.

  1. Recon first. Read nearby tests, fixtures, factories, helpers, and the production code path before adding patterns.

  2. Identify the behavior story. Identify the rule and keep unrelated concerns separate. Write it down only when that helps resolve ambiguity.

  3. Pick examples. Check existing coverage, then choose missing normal, boundary, or failure cases that explain real product risk.

  4. Shape the code for observability. Use existing safe seams, including scoped legacy patches with reliable restoration and isolation. If none can prove the behavior safely, propose the smallest production refactor and perform it only within authorized scope. Do not use real side effects or leaked globals to avoid that limit.

  5. Write the test as documentation. Keep setup meaningful, action obvious, and assertions specific. Add a short comment only for history, invariants, or non-obvious domain tradeoffs.

  6. Verify failure and readability. Run the focused test. When feasible, make sure it fails before the fix or would fail for the guarded regression. Re-read it as onboarding material.

  7. Report plainly. Report the behavior covered, verification, and remaining risk; mention boundary decisions only when consequential. Omit empty sections and routine process narration.

Optional Helper

Use the helper as a fast review prompt scanner, not as a verdict:

python3 scripts/analyze_maintainable_tests.py /path/to/project
python3 scripts/analyze_maintainable_tests.py /path/to/project --json

The helper filters directory scans to conventional test paths plus annotated Rust source, while an explicitly supplied supported source file is inspected directly; recognized declarations are checked for a bounded set of name, assertion, fixture, double, coupling, wall-clock, real-sleep, randomness, and legacy-rationale signals, and network access is not detected. A quiet scan does not prove the suite is good, and a noisy scan does not prove the tests are wrong.

When Guidance Stops Helping

If a bundled testing pattern fails or conflicts with the repository's runner, check its installed version and official framework documentation. Prefer the idiomatic way to prove the behavior while retaining side-effect isolation; mark unexecuted checks honestly. Propose a canonical skill correction or deletion with the source/version and a distinguishing regression. Do not silently edit installed copies or add brittle tests merely to satisfy this skill's examples.

Reading Guide

NeedRead
Principles behind the defaultsreferences/principles.md
Naming tests as behavior documentationreferences/naming-and-intent.md
Arrange / Act / Assert, fixtures, datasets, and helpersreferences/structure-and-fixtures.md
Mocks, stubs, fakes, boundaries, and production-code decompositionreferences/doubles-and-boundaries.md
Legacy systems, characterization tests, and regression rationalereferences/legacy-and-characterization.md
Side-effect isolation, configuration matrices, exact artifact assertions, and compatibility testingreferences/side-effects-and-compatibility.md
Severity-first review of test diffsreferences/review-rubric.md
Common traps and anti-patternsreferences/gotchas.md
Source influence and adaptation notesreferences/source-notes.md

Gotchas

  1. A test can be short and still unreadable if the important story is hidden in factories or generic helpers.
  2. A test can be duplicated and still maintainable when the duplication keeps independent behaviors obvious.
  3. Mock-heavy tests often document implementation decisions, not product behavior. Use mocks when the interaction is the behavior.
  4. Parameterized tests become opaque when each row is just data. Name the cases and keep expected outcomes visible.
  5. Legacy tests without rationale freeze confusion. Say whether the behavior is intentional, historical, temporary, or unknown.
  6. Do not accept painful tests as inevitable. Pain can be design feedback from production code that needs a clearer boundary.
  7. A plausible matcher can still prove nothing when its subject has the wrong type or points at the wrong artifact. Trace every assertion back to the observable contract.
  8. Passing only on the newest dependency does not prove the advertised compatibility range. Test the oldest supported combination and optional-capability fallback.

Comments

Markdown supported