agentleFS
sourcehawksourcehawk/feature-dev-workflow/skills/testing-a-feature/SKILL.md

testing-a-feature

Skill2 starsChanged 12 days agoMITView on GitHub

Whenever you write a test. Especially when: - You just wrote a function and are about to "make sure it works." - You're tempted to copy an assertion from an existing test "to match the style." - You're about to rewrite a test because the implementation changed. Skip for generated test scaffolds where the assertions come straight from a tool you trust. This skill is about the assertion shape for a single surface — the unit and integration tests written…

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 5 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When an implementation changes but the contract doesn't, the tests should not change.

    line 25
  2. Distinctive:

    Before adding any assertion, open the surface under test and read its docstring / contract.

    line 33
  3. Distinctive:

    Don't test for behavior the contract doesn't promise.

    line 59
  4. Distinctive:

    Stop. Ask: did the contract change, or just the implementation?

    line 67
  5. Distinctive:

    Testing private functions directly: If the contract is private, the test is testing implementation.

    line 92

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

testing-a-feature

When to invoke

Whenever you write a test. Especially when:

  • You just wrote a function and are about to "make sure it works."
  • You're tempted to copy an assertion from an existing test "to match the style."
  • You're about to rewrite a test because the implementation changed.

Skip for generated test scaffolds where the assertions come straight from a tool you trust.

This skill is about the assertion shape for a single surface — the unit and integration tests written test-first as you implement. For a whole flow that crosses components toward a user- or consumer-visible outcome, see feature-dev-workflow:testing-end-to-end, which fires once the feature is structurally complete. Edge cases stay here, at the unit level; they do not belong in an end-to-end test.

Core principle

Tests assert intent, not implementation. The intent lives in the docstring, the function comment, the spec, the issue's acceptance criteria — the public contract of the surface under test. The implementation is the body of the function.

When an implementation changes but the contract doesn't, the tests should not change. When the contract changes, the tests change first (per TDD) and the implementation follows.

This is what makes a docstring valuable: it's the black box the tests verify against. Without an intentional docstring, you have nothing but the code to test against, and tests degrade into change-detectors.

Workflow

1. Re-read the contract

Before adding any assertion, open the surface under test and read its docstring / contract. If the docstring is absent or vague, that's the first bug to fix — a function without a contract can't be tested against intent.

For your project (customize this to your stack):

  • Public functions / methods: the doc comment is the contract.
  • Exported UI hooks / utilities: the type signature or doc comment is the contract.
  • Service endpoints / RPC handlers: the request/response schema is the contract for callers.
  • Config-driven behavior: the documented schema field is the contract.

2. Self-review the docstring against intent

A clean docstring should already enumerate the surface's promises. If reading it raises questions ("what happens when N is zero?", "does this retry on failure?", "is the result sorted?"), the docstring is incomplete. Fix the docstring first, then write the tests. Docstring-first surfaces the edge cases before any assertion is written.

REQUIRED SUB-SKILL: feature-dev-workflow:writing-code-comments before you write or change a docstring. It decides whether the docstring exists and what it holds. This skill tests against that contract.

OPTIONAL SUB-SKILL: simple-english:simple-english for the prose of the docstring and of any comment in the tests. If it is available, load it before you draft and write the sentences under it. Where its formatting rules (headings, bold, lists, or its register for chat replies) differ from the host skill or its template, the host skill and its template win. It sets sentence style only: the contract still decides what the docstring promises, and the project's comment rules still decide whether a comment exists and what it may say (no narration of the code, no change history). If the skill is not available, do not stop or wait for it. If you have not already done so in this session, tell the user once that it can be installed with /plugin marketplace add AminBlg/SimpleEnglish and then /plugin install simple-english@simple-english. Then write short, plain, active sentences without it.

3. List edge cases

For each behavior the contract promises, ask:

  • Happy path — the documented success case. Always test.
  • Boundary inputs — zero, one, many; empty string, single char, max length; null/absent vs empty collection.
  • Error paths — every error the contract names. Every error the contract doesn't name but the code clearly can return (then either name it in the docstring or make the code not return it).
  • Invariants — what should never happen, regardless of input. Concurrency safety, idempotency, atomicity.
  • Failure recovery — partial failure mid-call, retry semantics, what state survives.

Don't test for behavior the contract doesn't promise. If you're tempted to, the contract is incomplete — fix the contract first.

4. Write one test per edge case

Each test name reads as a contract statement — "persists multiple sessions across restart", "missing scope is actionable" — written in whatever naming convention the project already uses. The name describes the intent being verified, not the function being called. Each test's assertion is what the contract promises, not what the implementation happens to do today.

5. When tempted to rewrite a test

Stop. Ask: did the contract change, or just the implementation?

  • Contract changed → rewrite the test first, watch it fail for the right reason, then update the implementation.
  • Implementation changed → the test should still pass. If it doesn't, either the test was coupled to implementation details (bad test, fix it) or the implementation regressed the contract (bad change, revert it).

Rewriting a test "to match" a new implementation when the contract is unchanged decouples the test from intent and silently weakens the suite.

Edge-case discovery checklist

Apply per surface, per change:

  • Happy path — documented success case.
  • Empty / zero / absent inputs — what does the contract say happens? If it doesn't say, fix the contract.
  • Boundary values — first / last / single-element / off-by-one neighbors.
  • Error paths named in the contract — each one triggered and asserted.
  • Concurrency — clean under whatever race/thread detector the toolchain provides; concurrent writers if the contract promises safety.
  • Idempotency — does calling twice produce the same result the contract promises?
  • Persistence — does on-disk / on-network state survive restart, if the contract says so?
  • Partial failure — what state survives a mid-call error?

Anti-patterns

  • Testing the implementation, not the intent. A test that breaks when a private helper is renamed is testing implementation, not contract.
  • Copy-pasting an assertion "to match the file's style" without re-verifying that the asserted behavior is what the contract under test actually promises.
  • One mega-test per function. One assertion per intent. Mega-tests fail with one signal even when N intents are broken.
  • Testing private functions directly. If the contract is private, the test is testing implementation. Drive the private code via the public surface.
  • Absorbing test-setup pain instead of surfacing it. When standing the test up takes many collaborators stubbed, shared or global state to set and reset between tests, cross-module archaeology to construct a valid input, or reaching into internals to observe the result, that friction is the surface's design talking — not a testing problem. Building an ever-more-elaborate fixture hides the signal and locks the bad shape in. Surface it: fix the shape or the contract, or at minimum flag it as a finding on the PR. Do not ship the fixture as if the difficulty were normal.
  • Rewriting a test to make it pass against a changed implementation, without verifying the contract changed. This is how regressions slip through.
  • Skipping edge cases because "they're unlikely." A reviewer asks "what happens when N=0?" — the test should answer.

Red flags

ThoughtReality
"The function is too small to test"If it's worth writing, its contract is worth asserting.
"The test is fragile, let me loosen the assertion"Fragile = coupled to implementation. Tighten the contract or rewrite the test against intent.
"I'll skip the absent case, the caller will always pass a value"The contract should say "never call with an absent value" then. If it doesn't, test the absent case.
"The docstring is wrong but the implementation is right"Fix the docstring (or the implementation, if the docstring is the source of truth) before testing.
"Rewriting the test to match the new code is faster"And weakens the suite silently. Verify the contract changed first.
"Edge cases can wait for the next PR"They get forgotten. List them now even if you don't write them all.
"This is just fiddly setup, I'll write a fixture for it"Heavy setup — many stubs, global state to reset, reaching into internals — is a design signal. Escalate the shape; don't absorb it into a fixture.
"I had to read the implementation (or another module) to write the test"If you can't construct the inputs or predict the output from the contract alone, the contract or the interface is the bug. Fix that first.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.