Skip to content

How to Implement Test-Driven Development with Claude Code TDD Skill

Purpose

This post shows how to enforce test-driven development using Claude Code’s test-driven-development skill. The key point is that Claude writes tests first, then implementation, making TDD practice automatic.

The Problem

I know TDD is good practice. I’ve read the books. I’ve watched the talks. But when I sit down to code, I fall into the same trap.

Here’s what usually happens:

My brain: "Let me just write the function first, then I'll add tests."
30 minutes later: "The tests can wait until tomorrow."
Next week: Tests still don't exist.

The red-green-refactor cycle sounds great in theory. In practice, I skip the “red” phase entirely. I write code first, then backfill tests that pass trivially.

This defeats TDD’s purpose. Tests written after implementation rarely drive design. They just confirm what I already built.

I wanted a system that would force me to write tests first. Not through willpower, but through automation.

The Solution

Claude Code has a skill called test-driven-development. It enforces the red-green-refactor cycle automatically.

Installation

First, I installed the skill:

Terminal
npx skills add obra/superpowers@test-driven-development -y -g

The -g flag installs it globally, so it works across all my projects.

How It Works

When I request a feature, the skill forces this sequence:

  1. RED - Write a failing test first
  2. GREEN - Write minimal code to pass the test
  3. REFACTOR - Improve code while keeping tests green

Let me show you a real example.

Example: Email Validation

I asked Claude to implement email validation using TDD:

"Using TDD, implement a function that validates email addresses.
It should reject invalid formats and accept valid ones."

Step 1: RED Phase

Claude started by writing failing tests:

email.test.js
describe('validateEmail', () => {
it('should reject invalid emails', () => {
expect(validateEmail('invalid')).toBe(false);
expect(validateEmail('no@sign')).toBe(false);
expect(validateEmail('@nodomain.com')).toBe(false);
expect(validateEmail('[email protected]')).toBe(false);
});
it('should accept valid emails', () => {
expect(validateEmail('[email protected]')).toBe(true);
expect(validateEmail('[email protected]')).toBe(true);
});
});

Notice: no implementation exists yet. Claude ran the tests and they failed:

Test Output
ReferenceError: validateEmail is not defined

This is the “red” in red-green-refactor. A failing test proves the test actually tests something.

Step 2: GREEN Phase

Claude then wrote the minimal implementation:

email.js
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
module.exports = { validateEmail };

Tests passed:

Test Output
PASS email.test.js
validateEmail
✓ should reject invalid emails (3ms)
✓ should accept valid emails (1ms)

Step 3: REFACTOR Phase

Claude suggested improvements:

email.js (refactored)
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function validateEmail(email) {
if (typeof email !== 'string') {
return false;
}
return EMAIL_REGEX.test(email);
}
// Added edge case handling
function validateEmailStrict(email) {
if (!validateEmail(email)) return false;
const [localPart, domain] = email.split('@');
return localPart.length <= 64 && domain.length <= 255;
}
module.exports = { validateEmail, validateEmailStrict };

Tests still passed after refactoring. This is crucial. The test safety net lets me refactor confidently.

Why This Matters

TDD produces better code. Here’s what I noticed after using this skill for a week:

Better Design: Writing tests first forces me to think about interfaces. I design from the outside in, which produces cleaner APIs.

Fewer Bugs: Tests catch edge cases I would miss. The email validation example above now handles non-string inputs, which I initially forgot.

Confident Refactoring: With tests passing, I can improve code structure without fear. I know immediately if I break something.

Documentation: Tests serve as living documentation. Six months later, I can read the tests and understand what the code should do.

Common Mistakes

I made several mistakes when starting with this skill. Here’s what I learned:

1. Skipping the RED Phase

Sometimes I got impatient and asked Claude to write tests and implementation together:

# WRONG - Asking for both at once
"Write a validateEmail function with tests"

This defeats TDD. Claude might write trivial tests that always pass. The skill works because it forces the red phase first.

The correct approach:

# CORRECT - Explicitly invoke TDD
"Using TDD, implement email validation"

2. Writing Trivial Tests

Early on, I accepted tests like this:

Weak test
it('should work', () => {
expect(validateEmail('[email protected]')).toBe(true);
});

This tests one happy path. It doesn’t verify behavior boundaries.

Better tests cover edge cases:

Comprehensive tests
describe('validateEmail', () => {
it('should reject empty string', () => {
expect(validateEmail('')).toBe(false);
});
it('should reject null', () => {
expect(validateEmail(null)).toBe(false);
});
it('should reject undefined', () => {
expect(validateEmail(undefined)).toBe(false);
});
it('should reject spaces in email', () => {
expect(validateEmail('test @example.com')).toBe(false);
});
});

3. Refactoring Without Re-running Tests

After refactoring, I sometimes forgot to run tests. This is dangerous. Refactoring can introduce subtle bugs.

The skill reminds me to run tests after each refactor. I now make it a habit:

Terminal
npm test -- --watch

Running tests in watch mode during refactoring gives immediate feedback.

Integration with Existing Workflow

The TDD skill integrates with my development workflow:

With CI/CD

I added test coverage requirements to my CI pipeline:

.github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --coverage
- name: Check coverage
run: |
coverage=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
if (( $(echo "$coverage < 80" | bc -l) )); then
echo "Coverage $coverage% is below 80%"
exit 1
fi

This ensures TDD-produced code meets coverage standards.

With Pre-commit Hooks

I use husky to run tests before commits:

package.json
{
"husky": {
"hooks": {
"pre-commit": "npm test"
}
}
}

Now I can’t commit without passing tests.

Comparison: TDD Skill vs Manual TDD

Here’s how the TDD skill compares to manual TDD:

AspectManual TDDTDD Skill
Discipline requiredHighLow
Forgetting testsCommonRare
Test-first enforcementWillpower-basedAutomatic
ConsistencyVaries by dayAlways enforced

The skill removes the willpower component. TDD becomes the default behavior.

Summary

In this post, I showed how to use Claude Code’s test-driven-development skill to enforce TDD practice. The key point is that the skill automates the discipline of writing tests first.

Next steps:

  1. Install the skill: npx skills add obra/superpowers@test-driven-development -y -g
  2. Try it on your next feature with: “Using TDD, implement [feature]”
  3. Watch Claude write failing tests first, then implementation
  4. Refactor with confidence knowing tests protect you

This transforms TDD from a good intention into an automatic practice.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments