---
name: test-behaviour-not-implementation
description: Write and keep tests that check what callers can see - return values, errors, readable state and outside effects - not private helpers, call order or other implementation details. Judge each test with two edits - break the behaviour and it must fail, refactor the internals and it must pass. Use when tests break whenever internals change, or pass while the code is wrong.
license: MIT
metadata:
  category: debugging
  tags: "testing,quality"
  author: johnny-bravo
  version: "1.0"
  updated: "2026-11-04"
  status: published
  aliases: "principle test behavior not implementation, behavior not implementation, brittle tests, tests break on refactor, over-mocked tests, vacuous assertions, test the contract"
  source_url: "https://github.com/cursor/plugins/blob/ae5650febf4320793844c12c4adaa937f417f8da/pstack/skills/principle-test-behavior-not-implementation/SKILL.md"
  source_license: MIT
  copyright: "Copyright (c) 2026 Lauren Tan"
  original_author: "Lauren Tan (poteto), pstack plugin in cursor/plugins"
  modified: "Modified by TheSkillVault on 2026-11-04: the principle was rewritten as a step-by-step review with a two-edit check table, worked examples, gotchas, portability and done-when; no list of specific 'vacuous' matchers is given (an earlier upstream version listed some and outside issue #474 found the list inaccurate); British spelling. Not the original file."
  imported_on: "2026-11-04"
---

# Test behaviour, not implementation

A test is a promise about what the code does for its users: a caller, an end user or another service. If it checks internals instead, it breaks on every refactor and still misses real bugs. This skill checks each test against that promise.

## When to use
- Tests fail whenever you rename a helper or reorder code, but nothing is broken.
- Tests pass while the feature is clearly wrong.
- An AI wrote tests full of mocks, and you are not sure they prove anything.

## When not to use
- You need tests written from scratch with a framework. Use write-unit-tests-with-vitest, jest or pytest.
- You want missing tests on a branch found and ranked. Use audit-test-coverage-gaps-prompt.
- You are starting new code test-first. Use test-first-with-tdd, and apply this rule to each test you write.

## Steps
1. Name the users of the code under test and the interface they use, such as a function, an HTTP route or a CLI.
2. List what those users can observe: return values, raised errors, state they can read back and effects outside the code (a request sent, a row written, a message emitted).
3. For each test, imagine edit A: break the behaviour by returning a wrong value, skipping an effect or doing it twice. The test must fail.
4. Imagine edit B: change only the internals and keep every observable result. The test must still pass.
5. Keep tests that pass both checks. Rewrite or delete the others.
6. When rewriting, call the code through its real interface with a concrete input. Take the expected value from the requirement, not from running the code.
7. Run the suite, then make edit A for real on one function. Confirm the test goes red, and revert.

## Two-edit check
| Test passes edit A (still green when broken) | Test fails edit B (red after a refactor) |
|---|---|
| Assertion looser than the contract | Asserts a private helper was called |
| Expected value computed by the code under test | Asserts internal call order |
| Checks data the test built itself | Snapshots intermediate data |
| Restates a constant or prompt string without running the code that uses it | Pins an exact error message no caller reads |

## Gotchas
- The contract decides, not the shape of the assertion. If "an empty cart never charges" is the promise, "the payment client got no call" is behaviour. Pair it with a case where the charge happens.
- The payload sent to a mocked outside service is behaviour, because that service sees it. A call to your own helper is not.
- An empty list is a precise answer when nothing matches. It is a hollow check when the test just needs some result.
- Callers branch on the error type, so assert the type. Skip the exact wording unless users read it.
- An invariant can be the behaviour, such as a key that must exist in two tables, or a compile-time type check.
- Never put real credentials or customer data in fixtures. Use made-up values.

## Portability
Works with any language and test framework, such as Vitest, Jest, pytest, Go or JUnit. It also works as a review rule for AI agents: paste the two-edit check into your rules file.

## Done when
- Every test you touched fails when its behaviour breaks.
- The same tests pass after a refactor that keeps every result the same.
- No test asserts private helpers or call order unless that order is part of the contract.

## Example prompt
"Our tests break every time I refactor. Go through the cart tests and keep only ones that check behaviour a caller can see. Use the two-edit check and rewrite the rest."

---
Adapted from "principle-test-behavior-not-implementation" in cursor/plugins (pstack) (Copyright (c) 2026 Lauren Tan, MIT). Original: https://github.com/cursor/plugins/blob/ae5650febf4320793844c12c4adaa937f417f8da/pstack/skills/principle-test-behavior-not-implementation/SKILL.md. Changes by TheSkillVault are listed in the metadata above. See LICENSE.txt.
