
Tdd
- 1 installs
- 35 repo stars
- Updated April 29, 2026
- spences10/claude-code-toolkit
Drives implementation through the red-green-refactor TDD cycle, writing failing tests first and testing behavior at the module boundary.
About
Guides a red-green-refactor TDD workflow that writes failing tests before minimal implementation and tests behavior rather than internals. A developer uses it when explicitly doing test-driven development on a feature or bugfix.
- Test behavior at the public surface, not private methods or internal state
- Mock only at system boundaries like network, database, and file systems
Tdd by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,750 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/claude-code-toolkit --skill tddAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 35 |
| Last updated | April 29, 2026 |
| Repository | spences10/claude-code-toolkit ↗ |
What it does
Drives implementation through the red-green-refactor TDD cycle, writing failing tests first and testing behavior at the module boundary.
Files
Test-Driven Development
Drive implementation through tests using the red-green-refactor cycle.
Adapted from Matt Pocock's TDD skill
Trigger Patterns
- "use TDD"
- "TDD this feature"
- "red-green-refactor"
- "test-driven"
- "write tests first"
Do not activate for general "write tests for X" or "add test coverage" requests. This skill is specifically for the TDD workflow where tests lead implementation.
Philosophy
Test behavior, not implementation. Every test should verify what the code does from the outside, not how it works inside. See deep-modules.md for the reasoning behind this.
Good tests act as a contract: they define what the module promises to its callers. If you can swap the internals completely and your tests still pass, they're testing the right thing.
Workflow
1. Plan the Interface
Before writing any code, decide the public surface area:
- What functions/methods/endpoints will exist?
- What are the inputs and outputs?
- What are the edge cases and error conditions?
Design for the caller, not the implementation. See interface-design.md.
2. Write a Tracer Bullet Test
Start with one test that exercises the simplest meaningful path through the feature. This test should:
- Call the public API as a real consumer would
- Assert the most basic expected output
- Fail — because the implementation doesn't exist yet
RED: test exists, code does notRun the test. Confirm it fails for the right reason (missing function, wrong return value — not a syntax error).
3. Make It Green
Write the minimum code to pass that one test. Do not write more than what the test demands. Hardcode return values if that's all it takes. The goal is a green test suite, not elegant code.
GREEN: test passes with minimal implementation4. Refactor Under Green
With the test passing, clean up. Remove duplication, improve names, extract helpers — but only while tests stay green. See refactoring.md.
REFACTOR: improve code quality, tests still pass5. Repeat
Add the next test. Pick the next simplest behavior that isn't covered. Follow the same cycle:
1. Write a failing test 2. Make it pass with minimal code 3. Refactor while green
Build up complexity incrementally. Each cycle should take minutes, not hours.
6. Handle Edge Cases
After the core behavior works, add tests for:
- Invalid inputs and error conditions
- Boundary values
- Empty/null/undefined cases
- Concurrent access if relevant
7. Final Refactor
Once all behavior is covered, do a final pass:
- Look for patterns across tests — consolidate setup with helpers
- Check that test names clearly describe the behavior being verified
- Ensure no test depends on another test's state
- Review implementation for unnecessary complexity
When to Mock
Use real dependencies when practical. Mock only at system boundaries — network calls, databases, file systems, clocks. See mocking.md for detailed guidance.
Test Quality
Write tests that are readable, independent, and fast. See tests.md for patterns on naming, structure, and assertion style.
Anti-patterns
- Writing implementation before the test (defeats the purpose)
- Writing multiple tests before making any pass (too much red)
- Skipping the refactor step (accumulates mess)
- Testing private methods or internal state
- Making tests pass by weakening assertions
- Mocking everything instead of testing real behavior
References
- deep-modules.md - Why to test surface area, not internals
- interface-design.md - Contract-first design
- mocking.md - When and how to use test doubles
- refactoring.md - Safe refactoring under green tests
- tests.md - Test structure, naming, and assertions
Deep Module Testing
The Core Idea
A deep module has a simple interface hiding complex internals. When testing deep modules, test the simple interface — not the complexity underneath.
If your tests break every time you restructure internals, they're coupled to implementation. This makes refactoring terrifying instead of routine.
Test the Surface, Not the Guts
Think of every module as having two parts:
1. Surface area — the public API callers depend on 2. Internals — the implementation details that can change freely
Tests should exercise (1) and ignore (2).
Good: Testing Behavior
// Tests what the function promises
expect(parseCSV("name,age\nAlice,30")).toEqual([{ name: "Alice", age: "30" }])Bad: Testing Internals
// Tests how the function works internally
expect(parser._splitLines("a\nb")).toEqual(["a", "b"])
expect(parser._tokenize("a,b")).toEqual(["a", "b"])The Swap Test
Ask yourself: could I completely rewrite the internals and have my tests still pass? If yes, your tests are at the right level. If no, they're too coupled.
When Depth Changes
Sometimes a module's interface grows (more parameters, more methods). That's a signal the module may need splitting. When you split, each new module gets its own surface-area tests.
Private Functions
Private functions exist to support the public interface. Test them through the public interface. If a private function is complex enough that you feel it needs direct tests, it probably deserves to be its own module with its own public interface.
Designing Testable Interfaces
Contract-First Thinking
Before writing code, define the contract: what does this module accept, return, and guarantee?
A contract is:
- Inputs — what the caller provides (types, constraints, defaults)
- Outputs — what the caller gets back (shape, guarantees, invariants)
- Errors — what can go wrong and how the caller is told
Write your first test against this contract. The test documents the contract better than any comment.
Design for the Caller
Ask "how do I want to call this?" before asking "how will I implement this?"
Good: Caller-Friendly
const result = await sendEmail({ to: "user@example.com", subject: "Hello", body: "Hi there" })
// result: { sent: true, messageId: "abc-123" }Bad: Implementation-Leaking
const transport = new SmtpTransport({ host: "...", port: 587 })
const message = transport.createMessage()
message.setHeader("To", "user@example.com")
message.setHeader("Subject", "Hello")
message.setBody("text/plain", "Hi there")
const result = await transport.send(message)Keep Interfaces Narrow
Accept only what you need. Return only what the caller needs. Every extra parameter or return field is a commitment you have to maintain.
Options Objects Over Long Parameter Lists
When a function needs more than 2-3 arguments, use an options object. This makes tests more readable and lets you add optional parameters without breaking existing callers.
// Clear what's being tested
processOrder({ items: [...], shipping: "express", coupon: "SAVE10" })
// vs. positional mystery
processOrder([...], "express", null, null, "SAVE10")Idempotency
Where possible, design operations that produce the same result when called multiple times. Idempotent operations are dramatically easier to test — you don't need to worry about test ordering or cleanup.
Error Design
Decide early: does this function throw, return an error value, or use a Result type? Be consistent within a module. Tests for error cases should be as explicit as tests for happy paths.
Mocking Guidance
The Rule
Use real dependencies when you can. Mock at system boundaries when you must.
What to Mock
- Network calls — HTTP APIs, gRPC services, WebSocket connections
- Databases — unless you have a fast local instance for integration tests
- File system — when tests would create real files with side effects
- Time — when behavior depends on current time, timers, or delays
- Randomness — when you need deterministic output
- Third-party services — payment processors, email providers, auth services
What NOT to Mock
- Your own code — if you mock your own modules, you're testing wiring not behavior
- Data transformations — pure functions that take input and return output
- Utility functions — these are fast and deterministic already
- Standard library — trust the language runtime
Types of Test Doubles
Stubs
Return canned responses. Use when you need a dependency to return specific data.
const getUser = stub().returns({ id: 1, name: "Alice" })Spies
Record calls while executing real behavior. Use when you want to verify something was called without changing behavior.
const spy = spyOn(logger, "warn")
doThing()
expect(spy).toHaveBeenCalledWith("deprecation notice")Fakes
Simplified working implementations. Use for complex dependencies where stubs are insufficient. Example: an in-memory database that supports basic queries.
Mocks
Doubles with built-in assertions about how they're called. Use sparingly — they couple tests to implementation details.
Mock Pitfalls
- Mock drift — the mock stops matching the real dependency's behavior. Periodically run integration tests against the real thing.
- Over-mocking — every dependency mocked means you're testing your test setup, not your code. If a test has more mock setup than assertions, reconsider.
- Mocking what you don't own — wrap third-party APIs in your own adapter, then mock the adapter. This gives you a stable interface to mock.
Integration vs Unit
Not every test needs to be a fast unit test. A small number of integration tests with real dependencies catches the gaps that unit tests with mocks miss. Balance speed with confidence.
Refactoring Under Green Tests
The Safety Net
Refactoring means changing code structure without changing behavior. Green tests prove behavior is preserved. Never refactor with failing tests — you won't know if you broke something or if it was already broken.
When to Refactor
Refactor during the "refactor" step of red-green-refactor, specifically:
- After making a test green — clean up the code you just wrote
- Before starting a new test — if existing code is messy enough to slow you down
- At the end of a TDD session — final cleanup pass
What to Refactor
Remove Duplication
If the same logic appears in multiple places after a few TDD cycles, extract it. But wait until you have at least three instances — premature extraction creates the wrong abstraction.
Improve Names
After understanding the domain better through TDD cycles, rename variables, functions, and modules to reflect what you now know.
Simplify Conditionals
Nested if/else chains that grew during TDD can often be simplified into guard clauses, lookup tables, or polymorphism.
Extract Functions
When a function does too many things, extract focused helpers. Each helper should be testable through the parent's public interface.
Consolidate Test Setup
After several tests, you'll see repeated setup. Extract shared fixtures or builder functions. But keep each test's unique setup visible in the test itself.
What NOT to Refactor
- Code that isn't related to what you're currently building
- Tests that are passing and readable (don't over-engineer test infrastructure)
- Performance optimizations (profile first, optimize later, separate from TDD)
The Refactoring Loop
1. Run tests — confirm green 2. Make one structural change 3. Run tests — confirm still green 4. Repeat or stop
Keep changes small. If tests go red, undo immediately — the change was wrong.
Knowing When to Stop
Refactoring has diminishing returns. Stop when:
- Code reads clearly to someone new
- There's no obvious duplication
- Functions have single responsibilities
- You're about to add a feature nobody asked for
Test Structure and Patterns
Test Naming
Test names should describe the behavior being verified, not the implementation being exercised.
Good Names
"returns empty array when no items match filter"
"throws ValidationError when email is missing"
"applies discount before calculating tax"Bad Names
"test filter function"
"test error"
"test calculateTotal"A good name answers: what should happen and under what conditions.
Test Structure: Arrange-Act-Assert
Every test has three parts:
// Arrange — set up the scenario
const cart = createCart([{ item: "book", price: 10 }])
// Act — perform the action
const total = cart.checkout({ discount: 0.1 })
// Assert — verify the outcome
expect(total).toBe(9)Keep these sections visually distinct. Avoid mixing arrange and act or act and assert.
One Behavior Per Test
Each test should verify one thing. Multiple assertions are fine if they all verify aspects of the same behavior. But testing two separate behaviors in one test makes failures ambiguous.
Good
"applies percentage discount to subtotal"
"rejects discount codes that have expired"Bad
"applies discount and rejects expired codes"Test Independence
Every test must pass or fail regardless of which other tests run, or in what order. No test should depend on state left behind by a previous test.
- Reset shared state in setup/teardown hooks
- Create fresh instances in each test
- Never rely on test execution order
Assertion Patterns
Be Specific
// Good — exact expectation
expect(result).toEqual({ status: "ok", count: 3 })
// Bad — too loose
expect(result).toBeTruthy()
expect(result).toBeDefined()Test Error Cases Explicitly
expect(() => divide(1, 0)).toThrow("Cannot divide by zero")Don't just test that something throws — verify the right error with the right message.
Avoid Snapshot Tests for Logic
Snapshots are useful for UI rendering. They're harmful for business logic because they encourage approving changes without understanding them.
Test Hygiene
- Fast — each test under 100ms where possible. Slow tests get skipped.
- Deterministic — same input, same result, every time. No random data, no current time, no network.
- Self-contained — everything needed to understand the test is visible in the test body. Minimize indirection.
- No logic in tests — no if/else, no loops, no try/catch in test bodies. Tests should be straight-line code.
Describe/Context Grouping
Group related tests to reduce duplication and improve readability:
describe("UserService.create", () => {
describe("with valid input", () => {
test("creates user and returns id", ...)
test("sends welcome email", ...)
})
describe("with invalid input", () => {
test("rejects missing email", ...)
test("rejects duplicate username", ...)
})
})Test Data
Use minimal test data. Only include fields relevant to the behavior being tested. Use factory functions or builders for complex objects.
const user = buildUser({ role: "admin" }) // only role matters for this test