Module 1: Spec-First Methodology and TDD with AI

Your First Spec→Implement Cycle with Claude Code

Your First Spec→Implement Cycle with Claude Code

Capsule overview

You've learned the philosophy (why TDD with AI), the methodology (spec-first), and the technique (anatomy of a good test-spec). Now it's time to run it. In this capsule you're going to complete your first spec→implement→validate cycle with Claude Code — from start to finish, hands-on.

The cycle is simple: you write tests that define the behavior, you give them to Claude Code as context, Claude Code implements, and pytest validates. You'll run it step by step with a concrete example: a string utilities module. Deliberately simple so the focus is on the workflow, not on the complexity of the domain.

By the end, you'll have experienced the "aha" moment of spec-first: watching Claude Code implement exactly what your tests define, with no ambiguity, no interpretation. And you'll have lived the iteration loop: when a test fails, Claude Code reads the pytest output and fixes it automatically.


Setup: Preparing the Environment

Before you start, make sure your environment is ready.

Install pytest

# Create the working directory
mkdir spec-first-demo
cd spec-first-demo

# Create a virtual environment
python -m venv venv
source venv/bin/activate  # Mac/Linux
# venv\Scripts\activate   # Windows

# Install pytest
pip install pytest

Verify the installation

pytest --version
# Expected output (version may vary):
pytest 8.x.x

Project structure

spec-first-demo/
├── string_utils.py       ← Claude Code will create this file
├── test_string_utils.py   ← You will create this file
└── venv/

Step 1: Write the Tests First (You)

You're going to create a string_utils.py module with 3 functions. But you're NOT going to implement the functions — you're going to define their behavior with tests.

Create the file test_string_utils.py:

# test_string_utils.py
import pytest
from string_utils import reverse_string, count_vowels, capitalize_words


# === Tests for reverse_string ===

class TestReverseString:
    def test_reverse_simple_word(self):
        assert reverse_string("hello") == "olleh"
    
    def test_reverse_sentence(self):
        assert reverse_string("hello world") == "dlrow olleh"
    
    def test_reverse_empty_string(self):
        assert reverse_string("") == ""
    
    def test_reverse_single_char(self):
        assert reverse_string("a") == "a"
    
    def test_reverse_palindrome(self):
        assert reverse_string("racecar") == "racecar"
    
    def test_reverse_with_spaces(self):
        assert reverse_string("  hi  ") == "  ih  "


# === Tests for count_vowels ===

class TestCountVowels:
    def test_count_lowercase_vowels(self):
        assert count_vowels("hello") == 2
    
    def test_count_uppercase_vowels(self):
        assert count_vowels("HELLO") == 2
    
    def test_count_mixed_case(self):
        assert count_vowels("Hello World") == 3
    
    def test_no_vowels(self):
        assert count_vowels("rhythm") == 0
    
    def test_all_vowels(self):
        assert count_vowels("aeiou") == 5
    
    def test_empty_string(self):
        assert count_vowels("") == 0
    
    def test_numbers_and_symbols(self):
        assert count_vowels("h3ll0 w0rld!") == 0


# === Tests for capitalize_words ===

class TestCapitalizeWords:
    def test_capitalize_simple_sentence(self):
        assert capitalize_words("hello world") == "Hello World"
    
    def test_capitalize_single_word(self):
        assert capitalize_words("hello") == "Hello"
    
    def test_capitalize_already_capitalized(self):
        assert capitalize_words("Hello World") == "Hello World"
    
    def test_capitalize_all_lowercase(self):
        assert capitalize_words("the quick brown fox") == "The Quick Brown Fox"
    
    def test_capitalize_empty_string(self):
        assert capitalize_words("") == ""
    
    def test_capitalize_mixed_case(self):
        assert capitalize_words("hELLO wORLD") == "Hello World"

Notice what you did:

  • ✅ You defined 3 functions without implementing them
  • ✅ Each test is deterministic, independent, focused
  • ✅ The names document the behavior
  • ✅ You included edge cases (empty, one char, palindrome, no vowels)
  • ✅ The tests are your complete specification

Run the tests (they should fail)

pytest test_string_utils.py -v
# Expected output:
ERRORS - ModuleNotFoundError: No module named 'string_utils'

This is correct: string_utils.py doesn't exist yet. You're in the RED phase of TDD — the tests fail because the implementation doesn't exist.


Step 2: Give the Tests to Claude Code

Now you give your tests to Claude Code as context. There are several ways to do it:

Option A: In the Claude Code CLI

# If you're using Claude Code in the terminal:
claude

# Inside Claude Code, give it context:
> Read test_string_utils.py and create string_utils.py with the functions 
> reverse_string, count_vowels, and capitalize_words so that all 
> the tests pass.

Option B: In Cursor with Claude Code

If you're in Cursor, select test_string_utils.py as context and ask:

Create string_utils.py with the implementations of reverse_string, 
count_vowels, and capitalize_words. The tests in test_string_utils.py 
define the expected behavior — implement so they all pass.

What you're communicating to Claude Code

By giving it the tests, Claude Code receives:

From your tests, Claude Code infers:

reverse_string(str) -> str
- Reverses the entire string (not the words)
- Works with empty strings
- Preserves spaces in the reversed position

count_vowels(str) -> int
- Counts a, e, i, o, u (case-insensitive)
- Returns 0 for strings with no vowels
- Numbers and symbols are not vowels

capitalize_words(str) -> str
- First letter of each word uppercased
- Rest of each word lowercased
- Works with empty strings

You didn't have to write this specification in natural language — the tests communicate it implicitly.


Step 3: Claude Code Implements

Claude Code generates something similar to this:

# string_utils.py

def reverse_string(text: str) -> str:
    """Reverse the entire string."""
    return text[::-1]


def count_vowels(text: str) -> int:
    """Count vowels (a, e, i, o, u) in text, case-insensitive."""
    return sum(1 for char in text.lower() if char in "aeiou")


def capitalize_words(text: str) -> str:
    """Capitalize first letter of each word, lowercase the rest."""
    return " ".join(word.capitalize() for word in text.split(" ")) if text else ""

Notice:

  • Claude Code deduced the function signatures from the tests
  • The implementation is clean and direct
  • You didn't need to explain the logic — the tests defined it

Step 4: Run pytest (Validation)

pytest test_string_utils.py -v
test_string_utils.py::TestReverseString::test_reverse_simple_word PASSED
test_string_utils.py::TestReverseString::test_reverse_sentence PASSED
test_string_utils.py::TestReverseString::test_reverse_empty_string PASSED
test_string_utils.py::TestReverseString::test_reverse_single_char PASSED
test_string_utils.py::TestReverseString::test_reverse_palindrome PASSED
test_string_utils.py::TestReverseString::test_reverse_with_spaces PASSED
test_string_utils.py::TestCountVowels::test_count_lowercase_vowels PASSED
test_string_utils.py::TestCountVowels::test_count_uppercase_vowels PASSED
test_string_utils.py::TestCountVowels::test_count_mixed_case PASSED
test_string_utils.py::TestCountVowels::test_no_vowels PASSED
test_string_utils.py::TestCountVowels::test_all_vowels PASSED
test_string_utils.py::TestCountVowels::test_empty_string PASSED
test_string_utils.py::TestCountVowels::test_numbers_and_symbols PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_simple_sentence PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_single_word PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_already_capitalized PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_all_lowercase PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_empty_string PASSED
test_string_utils.py::TestCapitalizeWords::test_capitalize_mixed_case PASSED

========================= 19 passed in 0.02s =========================

19/19 tests pass. The implementation meets your specification exactly. You're in the GREEN phase of TDD.


Step 5: And When a Test Fails? (Iteration Loop)

The perfect cycle where everything passes on the first try is common with simple functions. But in real code, sometimes Claude Code generates an implementation that doesn't pass all the tests. Let's see how to handle that.

Let's simulate a failure

Imagine you add a more demanding test:

# Add this test to TestCapitalizeWords:
def test_capitalize_preserves_multiple_spaces(self):
    assert capitalize_words("hello   world") == "Hello   World"

You run pytest:

pytest test_string_utils.py::TestCapitalizeWords::test_capitalize_preserves_multiple_spaces -v
FAILED test_string_utils.py::TestCapitalizeWords::test_capitalize_preserves_multiple_spaces
  AssertionError: assert 'Hello World' == 'Hello   World'

Claude Code's implementation used text.split(" "), which collapses multiple spaces into one. Your test defines that multiple spaces should be preserved.

The iteration loop

You give the pytest output to Claude Code:

The test test_capitalize_preserves_multiple_spaces fails:
AssertionError: assert 'Hello World' == 'Hello   World'

The capitalize_words function is collapsing multiple spaces.
Fix it to preserve multiple spaces.

Claude Code fixes it:

import re

def capitalize_words(text: str) -> str:
    """Capitalize first letter of each word, lowercase the rest."""
    if not text:
        return ""
    return re.sub(
        r'\S+',
        lambda m: m.group().capitalize(),
        text
    )

You run pytest again:

pytest test_string_utils.py -v
========================= 20 passed in 0.02s =========================

The iteration loop worked:

  1. Test fails → pytest gives precise feedback
  2. You give the feedback to Claude Code → Claude Code understands exactly the problem
  3. Claude Code fixes it → pytest validates the fix

You didn't have to explain the bug in natural language. The pytest output was enough.


The Counter-Example: Without Tests

To understand the value of the spec-first cycle, compare it with the workflow without tests.

Without tests: What you would have done

1. "Claude, create a string_utils module with reverse_string, 
    count_vowels, and capitalize_words"

2. Claude generates the implementation

3. You test manually:
   >>> reverse_string("hello")
   'olleh'           # ✅ "Looks good"
   
   >>> count_vowels("hello")
   2                 # ✅ "Correct"
   
   >>> capitalize_words("hello world")
   'Hello World'     # ✅ "It works"
   
4. "Everything works" → push

5. Two weeks later:
   capitalize_words("hello   world") → "Hello World"  # ❌ Bug!
   count_vowels("café") → 2  # ❌ What about the é?

With tests: What you did

1. You wrote 19 tests that define ALL the behavior

2. Claude Code implemented so the tests pass

3. pytest validated in 0.02 seconds

4. When you found a new case (multiple spaces),
   you added a test, Claude Code fixed it, pytest validated

5. You have 20 tests as a permanent safety net
   → If someone changes string_utils.py, the tests catch regressions

Comparison: Test-First vs Test-After in This Example

AspectTest-First (what you did)Test-After (alternative)
StartYou wrote 19 testsYou asked for the implementation
Ambiguity0 — tests define behaviorHigh — "create string utils"
Edge casesDefined BEFORE implementingDiscovered AFTER (if ever)
Confidence19 tests pass = correct"Looks good" = hope
RegressionsTests catch changesManual testing (if you remember)
Cost of adding testsAlready writtenHave to write them later

The cost of test-first: ~15 minutes more to write the tests before implementing.

The benefit of test-first: Permanent automatic validation. If in 3 months someone (or Claude Code) changes string_utils.py, the 20 tests instantly verify that nothing broke.


Project Connection

In the Spec-first mini-app (this module's project):

  • You'll apply exactly this cycle to a calculator: you write tests → Claude Code implements → pytest validates
  • You'll experience real iteration loops when Claude Code doesn't cover all the edge cases on the first try
  • You'll practice giving pytest feedback to Claude Code for fixes

This is the workflow you'll use throughout the guide and in all your future development with Claude Code.


Troubleshooting

Problem 1: "Claude Code generated the implementation but the imports fail"

Cause: The implementation file has a different name than the tests expect, or it's in a different directory.

Solution: Verify that string_utils.py is in the same directory as test_string_utils.py. The tests do from string_utils import ... — the file must be named exactly string_utils.py.

ls -la *.py
# It should show:
# string_utils.py
# test_string_utils.py

Problem 2: "pytest doesn't find the tests"

Cause: The tests don't follow pytest's naming convention.

Solution: Verify that:

  • The file starts with test_ (e.g., test_string_utils.py)
  • The test functions start with test_ (e.g., def test_reverse_simple_word)
  • The classes start with Test (e.g., class TestReverseString)
# Run with verbose to see what pytest discovers:
pytest test_string_utils.py -v --collect-only

Problem 3: "Claude Code doesn't implement what my tests expect"

Cause: The tests may not give enough context. Claude Code infers the signature and behavior from the tests, but sometimes the inference is incorrect.

Solution: Add natural-language context along with the tests:

"Implement string_utils.py with these functions:
- reverse_string: reverses the whole string (not the individual words)
- count_vowels: counts a, e, i, o, u (case-insensitive)
- capitalize_words: first letter of each word uppercased, rest lowercased

The tests in test_string_utils.py define the exact behavior."

Problem 4: "The iteration loop feels slow"

Cause: You're manually copying the pytest output and pasting it into Claude Code.

Solution: In the Claude Code CLI, you can run pytest directly and Claude Code sees the output. In later modules (especially module 6) you'll learn about automatic validation loops where Claude Code runs, reads the output, and fixes without manual intervention.


Exercises

Exercise 1: Your own spec-first cycle (Easy)

Write 4 tests for a function is_palindrome(text: str) -> bool and then ask Claude Code to implement it. Run pytest to validate.

See solution
# test_palindrome.py
from palindrome import is_palindrome

def test_palindrome_simple():
    assert is_palindrome("racecar") == True

def test_not_palindrome():
    assert is_palindrome("hello") == False

def test_palindrome_case_insensitive():
    assert is_palindrome("Racecar") == True

def test_empty_string_is_palindrome():
    assert is_palindrome("") == True

Expected implementation from Claude Code:

# palindrome.py
def is_palindrome(text: str) -> bool:
    cleaned = text.lower()
    return cleaned == cleaned[::-1]
pytest test_palindrome.py -v
# 4 passed

Explanation: The tests define that is_palindrome must be case-insensitive and that an empty string is a palindrome. Claude Code infers both requirements from the tests.

Exercise 2: Add edge cases (Easy)

Take the tests from Exercise 1 and add 3 more edge cases. Run pytest — do they pass with the existing implementation?

See solution
# Additional tests:
def test_palindrome_single_char():
    assert is_palindrome("a") == True

def test_palindrome_with_spaces():
    assert is_palindrome("race car") == False  # Spaces count

def test_palindrome_numbers():
    assert is_palindrome("12321") == True
pytest test_palindrome.py -v
# 7 passed (if the original implementation is correct)

Explanation: test_palindrome_with_spaces defines that spaces DO count (they're not ignored). If you wanted "race car" to be a palindrome (ignoring spaces), the test would be assert is_palindrome("race car") == True — and Claude Code would implement the space cleanup.

Exercise 3: Experience the iteration loop (Medium)

Write a test that you KNOW will fail with a basic implementation:

def test_capitalize_words_with_apostrophes():
    assert capitalize_words("it's a beautiful day") == "It's A Beautiful Day"
  1. Add this test to your existing tests
  2. Run pytest — observe the failure
  3. Give the failure output to Claude Code
  4. Claude Code fixes it
  5. Run pytest again

Document each step of the iteration loop.

See solution

Step 1: Add the test

Step 2: pytest fails:

FAILED test_string_utils.py::TestCapitalizeWords::test_capitalize_words_with_apostrophes
AssertionError: assert "It'S A Beautiful Day" == "It's A Beautiful Day"

The problem: .capitalize() in Python uppercases the first letter and lowercases the REST — including the s after the apostrophe. But the test expects it's → It's (not It'S).

Step 3: You give Claude Code:

The test test_capitalize_words_with_apostrophes fails:
"It'S A Beautiful Day" != "It's A Beautiful Day"

.capitalize() converts it's → It'S. I need only the first 
letter of the word to be uppercase, without affecting the rest.

Step 4: Claude Code fixes it:

def capitalize_words(text: str) -> str:
    if not text:
        return ""
    
    def capitalize_first(word):
        if not word:
            return word
        return word[0].upper() + word[1:].lower()
    
    return re.sub(r'\S+', lambda m: capitalize_first(m.group()), text)

Hmm, but this converts it's → It's (correct) and also hELLO → Hello (which is what we want per test_capitalize_mixed_case).

Step 5: pytest passes all the tests.

Lesson from the iteration loop: The pytest feedback was enough for Claude Code to understand and fix the problem. You didn't need to explain the mechanics of .capitalize() — the test output did it for you.

Exercise 4: Spec-first for math utils (Medium)

Create a complete spec (8+ tests) for a math_utils.py module with these functions:

  • factorial(n: int) -> int
  • is_prime(n: int) -> bool

Write ONLY the tests. Don't implement the functions.

See solution
# test_math_utils.py
import pytest
from math_utils import factorial, is_prime


class TestFactorial:
    def test_factorial_zero(self):
        assert factorial(0) == 1
    
    def test_factorial_one(self):
        assert factorial(1) == 1
    
    def test_factorial_five(self):
        assert factorial(5) == 120
    
    def test_factorial_ten(self):
        assert factorial(10) == 3628800
    
    def test_factorial_negative_raises(self):
        with pytest.raises(ValueError, match="must be non-negative"):
            factorial(-1)


class TestIsPrime:
    def test_two_is_prime(self):
        assert is_prime(2) == True
    
    def test_three_is_prime(self):
        assert is_prime(3) == True
    
    def test_four_is_not_prime(self):
        assert is_prime(4) == False
    
    def test_one_is_not_prime(self):
        assert is_prime(1) == False
    
    def test_zero_is_not_prime(self):
        assert is_prime(0) == False
    
    def test_large_prime(self):
        assert is_prime(97) == True
    
    def test_negative_raises(self):
        with pytest.raises(ValueError, match="must be non-negative"):
            is_prime(-5)

Explanation: 12 tests that fully define factorial and is_prime:

  • Happy path (known values)
  • Boundary values (0, 1, 2)
  • Edge cases (negatives → ValueError)
  • Special case (1 is not prime)

Giving these tests to Claude Code will produce a correct and complete implementation.


Summary

In this capsule you learned:

  • ✅ The complete spec-first cycle: write tests → give to Claude Code → run pytest → iterate if it fails
  • ✅ Practical setup: create the project, install pytest, file structure
  • ✅ How to give tests as context to Claude Code (CLI or Cursor)
  • ✅ The iteration loop: pytest fails → you give the output to Claude Code → Claude fixes → pytest validates
  • ✅ Contrast with the workflow without tests: less confidence, more bugs, no safety net
  • ✅ Test-first costs ~15 minutes more but gives permanent automatic validation

Next capsule: Project — Spec-first mini-app. You'll apply everything you learned by building a complete calculator with spec-first methodology.


Additional Resources

  1. pytest: Usage and Invocations - pytest command-line options (-v, -k, etc.)
  2. pytest: Output Formatting - How to read and configure pytest output
  3. Anthropic: Claude Code Getting Started - Setup and first steps with Claude Code
  4. Real Python: Getting Started with pytest - A hands-on pytest tutorial
  5. TDD by Example (Kent Beck) - A classic reference for understanding the red-green-refactor cycle

Module 1, Capsule 05 — Testing with Claude Code Guide From spec to implementation in minutes — your first complete cycle