Backend Unit Testing and TDD with Claude Code

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:

  1. Backend Unit Testing Standards: a short document that sets out what a good unit test looks like for our backend services.
  2. The tdd skill: 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 (describe blocks).
  • Name tests by behavior: should<Outcome>_when<Condition> or the sentence form should return empty list when no orders exist. Names like test1 or testDiscount don’t tell you anything.
  • Use descriptive variables: premiumCustomer, not c1. Use expectedTotal / 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.json and 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:

  1. Behavior: what can a caller do now that they couldn’t before?
  2. Seam: which public interface do we test through? (route, exported function, CLI, event handler)
  3. Reality: what’s real and what’s mocked? Only system boundaries get mocked.
  4. 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”)
  5. 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.”
  • 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

:package: 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.

4 Likes