Most of us know we should write tests first. Most of us write the code first, ship it, and promise to add tests “later.” Later rarely comes.
This post covers two things that try to fix that:
- Backend Unit Testing Standards: a short document that sets out what a good unit test looks like for our backend services.
- The
tddskill: a Claude Code skill that walks you through a card (story, bug, or task) test-first, following those standards, one step at a time.
The zip file (link at the end) contains both.
Part 1: The Documentation
Backend-Unit-Testing-Standards.pdf is the reference. It doesn’t push one tool on everyone. It’s a shared baseline so tests look and behave the same way across services, and so code reviews have something concrete to check against.
It covers unit tests only. Integration, contract, and E2E standards will get their own doc.
What’s inside
1. Principles: FIRST
Every unit test should be:
| Fast | Milliseconds. No network, disk, sleep(), or real DB. |
| Independent | No reliance on another test’s state or on run order. |
| Repeatable | Same result on your laptop, in CI, anywhere. |
| Self-validating | Pass or fail on its own. Nobody has to read the output to decide. |
| Timely | Written close to the code, ideally before it. |
2. TDD (Red → Green → Refactor)
- Red: write a failing test for the behavior you want.
- Green: write the minimum code to make it pass.
- Refactor: clean up while the tests stay green.
TDD is recommended, not a merge gate. The hard rule is simpler: every new piece of logic ends up with tests, whether you wrote them before or after. Bug fixes always get a regression test.
3. Structure: Arrange / Act / Assert
test('should apply 10% discount when customer is premium', () => {
// Arrange
const premiumCustomer = buildCustomer({ tier: 'premium' });
// Act
const total = calculateTotal(premiumCustomer, 100);
// Assert
expect(total).toBe(90);
});
- Test one logical thing per test.
- No
ifs or loops inside a test. Use parameterized tests (test.each) instead. - Use literal expected values (
90). Don’t recompute the answer with production logic.
4. Layout and naming
- Mirror the source tree. One test file per module:
discountService.js→discountService.test.js. - Group tests by function, then by scenario (
describeblocks). - Name tests by behavior:
should<Outcome>_when<Condition>or the sentence formshould return empty list when no orders exist. Names liketest1ortestDiscountdon’t tell you anything. - Use descriptive variables:
premiumCustomer, notc1. UseexpectedTotal/actualTotal.
5. Mocking: mock at the boundaries, not everywhere
| Mock it | Don’t mock it |
|---|---|
| HTTP calls, third-party APIs | Pure functions |
| Database (in unit tests) | Value objects, DTOs |
| Queues / event buses | Cheap, side-effect-free internal collaborators |
| Clock / time, random / UUID |
Anti-patterns to flag in review: mocking everything, mocking code you fully own, asserting only on call counts, and shared mock state that isn’t reset between tests.
6. Coverage
- ≥ 85% for business logic, utilities, and handlers.
- 80% overall per service, as a guardrail.
- Coverage is a signal, not a target. A test that runs a line without asserting anything gets flagged in review.
- Always test: public service methods, error paths, boundaries (null, empty, zero, max), business rules with branches, and every bug fix.
- If a PR drops coverage below the threshold, it needs reviewer sign-off. It isn’t auto-blocked.
7. Quality, data, and CI
- Every test has at least one meaningful assertion. Specific assertions beat
toBeTruthy(). - Test through the public interface, not private internals.
- Use builders and fixtures with sensible defaults (
orderWithNoItems()), and override only what the test cares about. - Unit tests need no network, credentials, or external services. They run locally before a PR and in CI on every push.
8. Recommended stack
| Language | Test | Mocking | Coverage |
|---|---|---|---|
| Node.js / TS | Jest | Jest mocks / sinon | Istanbul (built into Jest) |
| Python | pytest | unittest.mock / pytest-mock | coverage.py |
Existing services keep their current framework. New services start with this table.
9. Review checklist
The doc ends with a checklist reviewers can run through on any PR with tests: AAA, behavior-based names, correct location, boundary-only mocks, edge cases covered, real assertions, no order dependence, fast and deterministic, and a regression test for every bug fix.
It’s a living document, and changes go through a PR like any other code.
Part 2: The tdd Skill
A standards doc is good to have, but knowing the rules doesn’t mean you’ll follow them on a Friday afternoon with a card due. The tdd skill handles that part. It’s a Claude Code skill that takes a card and runs it through a test-first workflow. It won’t let you (or Claude) skip ahead to writing code.
Scope: JavaScript / TypeScript projects (needs a
package.jsonand a JS test runner like Jest, Vitest, or Mocha).
What it does
You paste a card. The skill:
- checks that the card actually belongs to the repo you’re in
- figures out what kind of work it is
- runs your existing suite to make sure you’re starting from green
- asks you a few short, focused questions to agree on what to test and where
- writes a test plan
- drives the red → green loop one small slice at a time
- hands you a comment to paste on the card, so PR reviewers can check the tests against the plan
How it works
The skill is a fixed sequence of states. It can’t jump forward:
INPUT → ORIENT → CLASSIFY → GROUND → READINESS GATE → GRILL
→ USER CONFIRMATION → TEST PLAN → RED/GREEN LOOP → REVIEW → EMIT
No test or production code is written until the test plan is confirmed. Saying “just implement it” doesn’t skip anything. It starts the workflow from the top.
Here’s what each step does:
| State | What happens |
|---|---|
| Input | You paste the card. If you paste nothing, it asks once and waits. |
| Orient | Checks the card fits this repo’s domain and finds a plausible seam. If it doesn’t fit, it stops and asks: wrong repo, new domain, or missing context? |
| Classify | Picks one mode: greenfield, retrofit, or bugfix. |
| Ground | Runs scripts/ground.sh: detects the package manager and test runner, runs the suite, and verifies the single-test command. If the suite is already red, it stops. You can’t tell your new red from old red. |
| Readiness gate | Six items must be settled: behavior, seam, single-test command, existing test conventions, expected value with its source, and what’s real vs. mocked. |
| Grill | Up to 5 questions, one at a time, each with a recommended answer and a one-line reason. You pick Go with the recommendation, Let me clarify, or Let’s chat about it. |
| User confirmation | Shows the full set of answers back to you and asks for one explicit “yes.” |
| Test plan | Writes the agreed plan (seams, slices as literal test names, expected values). |
| Red/Green loop | One slice at a time: failing test → confirm it fails for the right reason → minimal code → green → full suite still green. |
| Review | Checks every slice has a green test and nothing outside the agreed seams changed. Refactor ideas get noted for you, not applied silently. |
| Emit | Gives you a card comment: mode, seams, slice list, what was mocked. |
The five grill questions:
- Behavior: what can a caller do now that they couldn’t before?
- Seam: which public interface do we test through? (route, exported function, CLI, event handler)
- Reality: what’s real and what’s mocked? Only system boundaries get mocked.
- Expected value: what exact result, and where did that number come from? (card, worked example, observed response, or manual calculation; never “whatever the code returns”)
- Slice order: smallest observable slice first.
The three modes
- Greenfield: new behavior, no code yet. Full grill, then red → green per slice.
- Retrofit: code already exists.
- Written today, test skipped: write the test, then run
scripts/prove-test.sh. It deliberately breaks the source to confirm the test goes red, then restores it. If the test stays green while the logic is broken, the test is useless. - Old code the card changes: pin current behavior first with a characterization test, then TDD the change on top.
- Old code, no card, “just add coverage”: the skill pushes back and suggests waiting until a real card touches it.
- Retrofit work is always labelled
retrofit (characterization), never “TDD.”
- Written today, test skipped: write the test, then run
- Bugfix: reproduce, write a failing test that asserts the correct behavior, fix the minimum, go green. Short grill (~3 questions).
Typo, copy, or config-only change? The skill says so and gets out of your way.
Why “seams” instead of “unit test”?
The skill avoids the words “unit test” and “integration test” on purpose. Teams argue about those labels forever. A seam is simply the public boundary where behavior is observable: an HTTP route, an exported function, a CLI command, a message handler. Tests live at seams and never reach inside. That matches the standards doc: test through the public interface, and mock only at architectural boundaries.
What’s in the skill folder
.claude/skills/tdd/
├── SKILL.md # entry point + state machine
├── README.md # overview
├── grill.md # the 5-question interview
├── readiness.md # the 6-item gate
├── seams.md # what a seam is, how to rank candidates
├── mocking.md # boundary-only mocking, DB strategies
├── tests.md # good vs. bad test examples
├── modes/
│ ├── greenfield.md
│ ├── retrofit.md
│ └── bugfix.md
├── scripts/
│ ├── ground.sh # repo + suite health check
│ ├── find-seam.sh # locate candidate seams for a card
│ └── prove-test.sh # mutate source, confirm test catches it
└── templates/
├── test-plan.md
└── card-comment.md
How to Use It
1. Install
Download and unzip. Copy the tdd folder into your project:
mkdir -p .claude/skills
cp -r tdd-testing-skills/.claude/skills/tdd .claude/skills/
chmod +x .claude/skills/tdd/scripts/*.sh
Or install it globally, for all your projects:
mkdir -p ~/.claude/skills
cp -r tdd-testing-skills/.claude/skills/tdd ~/.claude/skills/
chmod +x ~/.claude/skills/tdd/scripts/*.sh
2. Make sure your baseline is green
Run your test suite once. If it’s already failing, the skill will stop at Ground and ask how you want to handle it.
3. Start Claude Code and hand it a card
claude
Then either invoke it directly:
/tdd
…or paste your card and mention TDD:
Let's work this card test-first:
As a user, I want checkPasswordStrength to return 'strong' for passwords
of 12+ characters that mix case and digits, so the register endpoint can
reject weak passwords.
4. Answer the grill
You’ll get one question at a time, like this:
❓ Q2 - Seam: password_checker.js:checkPasswordStrength called directly,
or through POST /register?
➡️ checkPasswordStrength directly. register.js doesn't call it yet,
so testing through the route would test code that doesn't exist.
Pick Go with the recommendation, Let me clarify, or Let’s chat about it. Say “go with your recommendations” once and it accepts the rest.
5. Confirm the plan, then watch red → green
After you confirm, the skill writes the test plan and works through the slices:
test('should return "strong" when password is 12+ chars with mixed case and digits', () => {
expect(checkPasswordStrength('Abcdef123456')).toBe('strong');
});
Red (returns undefined) → minimal implementation → green → full suite green. Then the next slice.
6. Paste the card comment
At the end you get something like:
**mode:** greenfield
**seams:**
- `password_checker.js:checkPasswordStrength`
**slices (test names, in order):**
1. `should return "strong" when password is 12+ chars with mixed case and digits`
**faked at boundaries:** nothing (no system boundary crossed)
**out of scope:** 'medium' tier (follow-up card)
Paste it on the card. During PR review, compare the test names in the diff against this list. If they don’t match, a step got skipped.
Quick tips
- Give it a real card. The more specific the card, the better the candidate answers. But it’ll still ask you to confirm each one.
- Don’t fight the stops. A red baseline or a “this card doesn’t belong here” stop is the skill doing its job.
- Expected values need a source. “Run it and see what it returns” isn’t one. That’s how you end up with a test that can never fail.
- Use the standards doc in review. The skill gets you to a good test; the checklist in section 13 of the PDF helps you keep it that way.
Get it
Download: TDD Skill
The zip contains:
tdd/Backend-Unit-Testing-Standards.pdf: the standards doc./tdd/skills: the Claude Code skill
Try it on your next card and let us know what worked and what got in your way. Both the doc and the skill will keep changing based on what you tell us.