← All skills

Behaviour, Not Coverage

Turns “add some tests” into tests that state what the code promises, instead of tests that exist to move a percentage.

testingqualityrefactoringnamingminutes

When to reach for it

When you are about to ask for tests on code that has none, or when coverage is high and you still do not trust the change enough to ship it on a Friday.

What changes

  • Every test is named as a sentence about behaviour, so a red run tells you what broke before you open the file.
  • Assertions land on what a caller can observe — a return value, a written row, an emitted event, rendered text — not on which private helper ran.
  • Behaviours the code promises but nothing checks are written out as a list, so a gap is a line you can read instead of a number below 100.
  • You can restructure the implementation without changing behaviour and the suite stays green, because the tests stopped being a second copy of the code.
  • Tests that only walk lines — getters, constructors, generated files — get skipped rather than written, so the suite gets smaller as trust goes up.

Pairs with

SKILL.mdpaste into your agent

Behaviour, Not Coverage

Tests exist to describe what the code promises. Coverage is a by-product of doing that well. Work in this order.

1. List the promises before writing a test

For the code in front of you, write the sentences a caller relies on:

  • The ordinary case ("splits a paid invoice across two line items")
  • The boundaries ("an empty cart totals zero rather than erroring")
  • The failures ("an expired token is rejected without a database hit")
  • The things that must not happen ("a declined charge writes no order")

If you cannot write four such sentences, you do not understand the code well enough to test it yet. Read it again, or ask.

2. Turn each promise into the test name

The name is the promise, in the words the runner will print on failure:

  • rejects an expired token — a failure tells you what broke
  • test handleAuth — a failure tells you only where to start reading

3. Assert on what a caller can observe

The return value, the persisted row, the published message, the HTTP response, the text on screen. Not private state, not the order of internal calls, not that a helper was invoked.

The check: could someone rewrite the implementation, keep every promise, and leave this test green? If not, the test is a copy of the code and will go red on every refactor for no reason worth having.

4. Say what you are not testing, and why

Write that list too. "Retry backoff timing is not covered" is a known gap someone can decide about. An uncovered path nobody has named is not.

5. Prove each test can fail

Break the behaviour on purpose — invert a condition, return a constant — and confirm the test goes red. A test that passes against broken code is worse than no test, because it is counted.

Rules

  • Do not write a test to move a number. If a line is uncovered and no promise needs it, the honest options are "name the missing promise" or "delete the code", never "add a test that runs it".
  • Do not name a test after the function it calls. Name it after the behaviour that would be missed if it vanished.
  • Do not assert that a mock was called. That tests your test.
  • Do not test the language, the framework or the ORM. They have suites of their own, and yours will only break when they upgrade.
  • Do not chase a percentage across generated code, plain accessors or configuration. Exclude them and say in one line that you did.