Skip to content
Back to skills

Tdd

ASecurity

What makes a test worth keeping — seams, assertions, mocking, and the anti-patterns that produce tests which pass forever and catch nothing. A reference consulted before and while writing a test, not a procedure. Use when writing or reviewing tests, choosing where to test, or judging whether a test earns its maintenance.

  • 4 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 29, 2026
developmentrustgoapidatabase

Works with

  • api

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add savinofiore/lexi --skill tdd --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tdd?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tdd
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/savinofiore-tdd/badge)](https://www.skillsdirectory.com/skills/savinofiore-tdd)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tdd
description: What makes a test worth keeping — seams, assertions, mocking, and the anti-patterns that produce tests which pass forever and catch nothing. A reference consulted before and while writing a test, not a procedure. Use when writing or reviewing tests, choosing where to test, or judging whether a test earns its maintenance.
---

# tdd — tests worth keeping

A test earns its place by two properties: it fails when behaviour breaks, and it
stays quiet when only structure changes. Everything below serves those two and
nothing else.

This is a reference, not a procedure — the loop, the gate and the stop
conditions live in the `lexi` skill. Consult this before writing a test and
while writing it, not in review afterwards.

## Seams: where a test attaches

A **seam** is a boundary where behaviour is observable from outside: a public
function, a module's exported API, a request handler, a repository method.
Tests attach at seams and never reach behind them.

Behind the seam is implementation — private helpers, intermediate state, the
order of internal calls. A test that reaches there breaks on a rename and
survives a real regression, which is the exact inverse of what you want.

Choosing seams is a decision, not a discovery. Too shallow, every private
function pinned, and the suite ossifies: no refactor is possible without a test
rewrite. Too deep, only the top-level entry point, and a failure tells you the
system is broken but not where. Pick the boundary where the behaviour has a
name, agree it, and put the test there.

## Naming: the test states what the system does

A test name is a sentence about capability: *parses an ISO date without a
timezone as UTC*, *rejects a coupon after its expiry*. Read top to bottom, the
suite should list what the unit can do.

`works`, `test2`, `handles edge case` describe nothing. When a name is hard to
write, the test is usually covering more than one behaviour — split it.

## Assertions come from outside the code

An expected value must have an independent source: a literal worked out by
hand, an example from the spec, a fixture captured from the real system.

Never recompute the expectation the way the implementation computes it:

```
expect(total(items)).toBe(items.reduce((a, i) => a + i.price, 0))   // asserts nothing
expect(total(items)).toBe(47.50)                                    // asserts something
```

The first agrees with the code by construction and can never disagree with it —
it is a copy of the implementation wearing a test's clothes. The same applies to
a snapshot generated by the code under test and approved without being read.

## Mocking

Replace what you don't own; use the real thing for everything else.

Network, clock, filesystem, randomness, third-party SDK → replace, because they
make the test slow or non-deterministic. Your own domain objects, pure
functions, value types → never: a mock of your own code asserts that your code
calls itself in a particular way, which is mechanism, not behaviour.

Rule of thumb: if swapping a collaborator for a mock changes *what the test
proves*, you are mocking too deep.

Prefer the narrowest double that works. A stub returning a value beats a mock
verifying call order — `verify(x.wasCalledOnce())` is a claim about how, and
how is free to change. Assert on the observable result instead.

## Anti-patterns

- **Implementation-coupled** — asserts on internals, private methods, or reads
  state through a side channel (querying the database instead of going through
  the interface). Tell: a refactor breaks it while behaviour is unchanged.
- **Tautological** — the assertion recomputes the expectation the way the code
  does, so it passes by construction.
- **Horizontal slicing** — all the tests first, then all the implementation.
  Bulk tests verify *imagined* behaviour: they commit to a shape before the
  implementation has taught you anything, and they go insensitive to the changes
  that matter. Work vertically — one test, one implementation, repeat.
- **Assertionless** — exercises the code and asserts nothing, or asserts
  something true regardless (`expect(result).toBeDefined()`).
- **Conditional** — an `if` in a test body is either two tests, or a test that
  quietly skips its own assertion for some inputs.
- **Shared mutable fixture** — order-dependent suites that pass alone and fail
  in CI. Each test builds what it needs.

## Excuses that sound like reasons

Each feels true in the moment; none changes what the test is for. When one is
the reason you are about to skip a step, write the test instead.

- *"Too simple to test."* Simple code still breaks, and its test is the cheapest.
- *"I'll add the test after."* A test written after passes on its first run, so
  it never proved it can fail.
- *"I checked it by hand."* A manual check leaves nothing behind; the next change
  undoes it unseen.
- *"The test is wrong, not the code."* Maybe. Then it is a breaking change: agree
  it first, never edit the assertion to reach green.
- *"It's urgent."* An urgent fix that breaks again is two incidents; the red test
  costs minutes.
- *"The user said to skip it."* Not an option lexi offers: the test comes first
  whoever asks.

## Coverage

Coverage reports what was executed, never what was verified: a line run by a
test with no assertion counts as covered and is not tested. Use it to find blind
spots. Never set it as a target — a number chased directly gets met with
assertionless tests, and then the suite is worse than before *and* looks better.

## When not to write one

Test the branch, the parser, the boundary, the money path, the thing you had to
think about. Skip the one-line delegation, the generated code, the constant, the
getter.

A suite that tests everything is a suite nobody maintains, and an unmaintained
test that fails gets deleted rather than fixed. Fewer tests that genuinely bite
beat a wall of them that nobody trusts.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.