Back

Superpowers Tutorial 4: test-driven-development (TDD with AI in Practice)

test-driven-development is a core Superpowers skill that enforces the RED→GREEN→REFACTOR loop. This guide shows how to do TDD with an AI coding assistant: test-first constraints, good test granularity, common anti-patterns, and practical workflows you can reuse.

Tutorial Overview

Series index: Superpowers Tutorial Series

test-driven-development is one of the core and strictest skills in the Superpowers framework. It forces AI to follow the classic TDD flow: write tests first, watch them fail, then write code.

What you will learn

  • ✅ The RED-GREEN-REFACTOR loop
  • ✅ How Superpowers enforces TDD
  • ✅ Reasonable test coverage standards
  • ✅ Common anti-patterns and how to avoid them
  • ✅ A practical end-to-end example

Why Is TDD So Important?

Problems with traditional development

flowchart LR
    A[Write code] --> B[Manual testing]
    B --> C[Find a bug]
    C --> D[Fix the bug]
    D --> E[Introduce a new bug]
    E --> F[Regression tests fail]
    
    style A fill:#ffcccc
    style E fill:#ffcccc
    style F fill:#ffcccc

The TDD flow

flowchart TD
    A[RED: Write a failing test] --> B[GREEN: Write the minimum code to pass]
    B --> C[REFACTOR: Improve the code]
    C --> D{Any more requirements?}
    D -->|Yes | A
    D -->|No | E[Done]
    
    style A fill:#ffebee
    style B fill:#e8f5e9
    style C fill:#e3f2fd

Benefits of TDD

Metric Traditional development TDD
Bug density High 60-80% lower
Regression issues Frequent Rare
Code quality Unstable Stable
Documentation value Low High (tests as docs)
Refactoring confidence Low High

RED-GREEN-REFACTOR Explained

Phase 1: RED - Write a failing test

Goal: Write a test that fails and describes the expected behavior

Key principles:

1. Write only the test needed to describe one behavior
2. Do not think about implementation details
3. Tests should be concise and clear
4. Test names should describe intent

Example:

# Bad test - tests too many behaviors
def test_tag_creation_and_validation_and_slug():
    tag = Tag(name="Test")
    assert tag.name == "Test"
    assert tag.slug == "test"
    tag.save()
    assert tag.id is not None

# Good test - one behavior only
def test_tag_creation_sets_name():
    tag = Tag(name="Test")
    assert tag.name == "Test"

def test_tag_creation_generates_slug():
    tag = Tag(name="Test Tag")
    assert tag.slug == "test-tag"

Phase 2: GREEN - Write the minimum code to pass the test

Goal: Make the test pass in the simplest way possible

Key principles:

1. Do not overdesign
2. You may write a "fake" implementation
3. YAGNI - do not build extra things
4. If the test is too complex, go back to RED and split it

Example:

# Test
def test_tag_slug_is_url_friendly():
    tag = Tag(name="Hello World!")
    assert tag.slug == "hello-world"

# ✅ The simplest implementation (it may be silly, but it works)
class Tag:
    def __init__(self, name):
        self.name = name
        self.slug = "hello-world"  # Hard-coded, but the test passes!

# The next test will force us to implement the real logic

Phase 3: REFACTOR - Improve the code

Goal: Improve code quality without changing behavior

Refactoring timing:

- Duplicate code appears
- Naming is unclear
- Functions are too long
- A class has too many responsibilities
- Common logic can be extracted

Refactoring checklist:

- [ ] Tests still pass
- [ ] No new warnings introduced
- [ ] Code is clearer
- [ ] No over-optimization

Superpowers TDD Skill Explained

When it triggers

The TDD skill triggers automatically in these scenarios:

  1. Implementing any feature - no matter how small
  2. Fixing bugs - write a reproduction test first
  3. Adding a new endpoint - write API tests first

Enforcement mechanism

flowchart TD
    A[AI is about to write code] --> B{TDD check}
    B -->|No test | C[❌ Block: write tests first]
    B -->|Has tests | D[Allow implementation]
    C --> E[Write tests]
    E --> F[Run tests and see them fail]
    F --> G{Tests fail?}
    G -->|No | H[❌ Tests are wrong]
    G -->|Yes | D
    D --> I[Write implementation]
    I --> J[Run tests]
    J --> K{Tests pass?}
    K -->|No | I
    K -->|Yes | L[REFACTOR]
    
    style C fill:#ffcccc
    style H fill:#ffcccc
    style L fill:#e8f5e9

TDD rules AI must follow

tdd:
  rules:
    - "Always write tests first"
    - "Only write implementation after the tests fail"
    - "Only write the minimum code needed to pass"
    - "Refactor after it passes"
    - "Delete any code written before the test"
  
  enforcement:
    - "If the user asks to write code directly, explain the TDD flow"
    - "If the test is hard to write, the design is probably wrong"
    - "If a test exceeds 20 lines, split it up"

Practical Example: Tag Validation

Requirement background

Add validation to the tag system:

  • Tag name cannot be empty
  • Tag name length must be 1-50 characters
  • Tag name may contain only letters, numbers, spaces, and hyphens

Full TDD flow

Cycle 1: Empty tag name validation

RED - Write the test

# tests/test_tag_validation.py

def test_tag_name_cannot_be_empty():
    """Tag names cannot be empty"""
    with pytest.raises(ValidationError) as exc_info:
        Tag(name="")
    
    assert "name" in str(exc_info.value)
    assert "cannot be empty" in str(exc_info.value)

Run the test (expected to fail)

$ pytest tests/test_tag_validation.py::test_tag_name_cannot_be_empty
FAILED - Tag() got an unexpected keyword argument 'name'

GREEN - Write the implementation

# app/Models/Tag.py

class Tag:
    def __init__(self, name: str):
        if not name:
            raise ValidationError("name cannot be empty")
        self.name = name

Run the test (pass)

$ pytest tests/test_tag_validation.py::test_tag_name_cannot_be_empty
PASSED

REFACTOR - No refactor needed


Cycle 2: Length validation

RED - Write the test

def test_tag_name_max_length_is_50():
    """Tag name max length is 50"""
    long_name = "a" * 51
    
    with pytest.raises(ValidationError) as exc_info:
        Tag(name=long_name)
    
    assert "max length" in str(exc_info.value).lower()

def test_tag_name_exact_max_length_works():
    """Exactly 50 characters should work"""
    name = "a" * 50
    tag = Tag(name=name)  # Should not raise an exception
    assert tag.name == name

Run the test (fail)

$ pytest tests/test_tag_validation.py::test_tag_name_max_length_is_50
FAILED - No exception raised

GREEN - Write the implementation

class Tag:
    MAX_NAME_LENGTH = 50
    
    def __init__(self, name: str):
        if not name:
            raise ValidationError("name cannot be empty")
        
        if len(name) > self.MAX_NAME_LENGTH:
            raise ValidationError(f"name max length is {self.MAX_NAME_LENGTH}")
        
        self.name = name

Run the test (pass)

$ pytest tests/test_tag_validation.py -v
test_tag_name_cannot_be_empty PASSED
test_tag_name_max_length_is_50 PASSED
test_tag_name_exact_max_length_works PASSED

REFACTOR - Extract validation methods

class Tag:
    MAX_NAME_LENGTH = 50
    
    def __init__(self, name: str):
        self._validate_name(name)
        self.name = name
    
    def _validate_name(self, name: str) -> None:
        if not name:
            raise ValidationError("name cannot be empty")
        
        if len(name) > self.MAX_NAME_LENGTH:
            raise ValidationError(f"name max length is {self.MAX_NAME_LENGTH}")

Cycle 3: Character validation

RED - Write the test

def test_tag_name_allows_letters_numbers_spaces_hyphens():
    """Tag names may contain letters, numbers, spaces, and hyphens"""
    valid_names = [
        "python",
        "machine learning",
        "web-dev",
        "AI and ML 2024",
        "test-123",
    ]
    
    for name in valid_names:
        tag = Tag(name=name)  # Should not raise an exception
        assert tag.name == name

def test_tag_name_rejects_special_characters():
    """Tag names reject special characters"""
    invalid_names = [
        "python!",
        "test@tag",
        "hello#world",
        "tag$name",
    ]
    
    for name in invalid_names:
        with pytest.raises(ValidationError) as exc_info:
            Tag(name=name)
        assert "invalid characters" in str(exc_info.value).lower()

Run the test (fail)

$ pytest tests/test_tag_validation.py::test_tag_name_rejects_special_characters
FAILED - No exception raised for 'python!'

GREEN - Write the implementation

import re

class Tag:
    NAME_PATTERN = re.compile(r'^[a-zA-Z0-9 -]+$')
    
    def _validate_name(self, name: str) -> None:
        if not name:
            raise ValidationError("name cannot be empty")
        
        if len(name) > self.MAX_NAME_LENGTH:
            raise ValidationError(f"name max length is {self.MAX_NAME_LENGTH}")
        
        if not self.NAME_PATTERN.match(name):
            raise ValidationError("name contains invalid characters")

Run the test (pass)

$ pytest tests/test_tag_validation.py -v
... (all tests pass)

REFACTOR - Final refactored code

import re
from typing import Final

class Tag:
    """Tag model class"""
    
    MAX_NAME_LENGTH: Final[int] = 50
    NAME_PATTERN: Final[re.Pattern] = re.compile(r'^[a-zA-Z0-9 -]+$')
    
    def __init__(self, name: str):
        self._validate_name(name)
        self.name = name
        self.slug = self._generate_slug(name)
    
    def _validate_name(self, name: str) -> None:
        """Validate the tag name"""
        self._validate_not_empty(name)
        self._validate_length(name)
        self._validate_characters(name)
    
    def _validate_not_empty(self, name: str) -> None:
        if not name:
            raise ValidationError("name cannot be empty")
    
    def _validate_length(self, name: str) -> None:
        if len(name) > self.MAX_NAME_LENGTH:
            raise ValidationError(f"name max length is {self.MAX_NAME_LENGTH}")
    
    def _validate_characters(self, name: str) -> None:
        if not self.NAME_PATTERN.match(name):
            raise ValidationError("name contains invalid characters")
    
    def _generate_slug(self, name: str) -> str:
        """Generate a URL-friendly slug"""
        return name.lower().replace(' ', '-')

Reasonable Test Coverage Standards

Superpowers coverage requirements

test_coverage:
  minimum:
    models: 90%      # Highest bar for models
    services: 85%    # Business logic
    controllers: 80% # Controllers
    utils: 95%       # Utility functions
  
  focus_on:
    - "edge cases"
    - "error handling"
    - "business rules"
  
  dont_worry_about:
    - "getter/setter"
    - "configuration classes"
    - "plain data classes"

Test priority matrix

                    Importance
              High ←─────────→ Low
            ┌─────────┬─────────┐
          High │ Must test │ Should test │
               │ (core logic) │ (supporting functionality) │
            ├─────────┼─────────┤
           Low │ Optional │ No need │
               │ (simple functions) │ (getter) │
            └─────────┴─────────┘

TDD Anti-Patterns

Anti-pattern 1: Oversized tests

# Bad - the test does too much
def test_complete_tag_workflow():
    tag = Tag(name="Test")
    assert tag.name == "Test"
    assert tag.slug == "test"
    tag.save()
    assert tag.id == 1
    tag.update(name="Updated")
    assert tag.name == "Updated"
    tag.delete()
    assert Tag.find(1) is None

# Good - one behavior per test
def test_tag_creation()
def test_slug_generation()
def test_tag_save()
def test_tag_update()
def test_tag_delete()

Anti-pattern 2: Test dependency

# Bad - tests depend on each other
def test_create_tag():
    global tag_id
    tag = Tag(name="Test")
    tag.save()
    tag_id = tag.id

def test_update_tag():
    tag = Tag.find(tag_id)  # Depends on the previous test!
    tag.update(name="Updated")

# Good - each test is independent
def test_create_tag():
    tag = Tag(name="Test")
    tag.save()
    assert tag.id is not None

def test_update_tag():
    tag = Tag(name="Test")
    tag.save()
    tag.update(name="Updated")
    assert tag.name == "Updated"

Anti-pattern 3: Too much mocking

# Bad - too many mocks, the test loses meaning
@mock.patch('database.Connection')
@mock.patch('cache.Redis')
@mock.patch('logger.Logger')
def test_tag_save(mock_logger, mock_redis, mock_db):
    tag = Tag(name="Test")
    tag.save()

# Good - only mock external dependencies when necessary
def test_tag_save():
    # Use a real database (test-only)
    tag = Tag(name="Test")
    tag.save()
    assert tag.id is not None

Anti-pattern 4: Testing private methods

# Bad - tests implementation details
def test_private_validate_name():
    tag = Tag.__new__(Tag)
    tag._validate_name("Test")

# Good - test through the public API
def test_invalid_name_raises_error():
    with pytest.raises(ValidationError):
        Tag(name="")

TDD Strategies for Common Scenarios

Scenario 1: Fixing a bug

Flow:

1. Write a test that reproduces the bug (it should fail)
2. Fix the bug
3. Make the test pass
4. Run regression tests to ensure nothing else broke

Example:

# Bug: slug generation breaks when the tag name contains spaces

# RED - reproduce the bug
def test_tag_slug_handles_spaces():
    tag = Tag(name="hello world")
    assert tag.slug == "hello-world"  # Bug: the real output is "hello world"

# GREEN - fix it
def _generate_slug(self, name: str) -> str:
    return name.lower().replace(' ', '-')

Scenario 2: Adding a new endpoint

Flow:

1. Write an API integration test (fail)
2. Implement the route and controller (pass)
3. Add business logic tests
4. Refactor

Scenario 3: Refactoring legacy code

Flow:

1. Add "characterization tests" for existing code
2. Make sure the tests pass (record current behavior)
3. Start refactoring
4. Keep the tests passing

Superpowers TDD Configuration

Configuration example

tdd:
  # Strict mode (default)
  enforcement: strict
  
  # Test framework
  framework: pytest  # or unittest, jest, etc.
  
  # Coverage requirements
  coverage:
    enabled: true
    minimum: 85%
    fail_below: true
  
  # When to run tests
  run_tests:
    - "before_implementation"
    - "after_implementation"
    - "before_commit"
  
  # Test generation
  test_generation:
    auto_generate: true
    include_edge_cases: true
    include_error_cases: true

It can be disabled temporarily in some situations:

tdd:
  enabled: false  # ⚠️ Prototype/demo only
  
# Or use rapid mode
tdd:
  mode: rapid  # Reduce the number of tests and cover only the main flow

Summary

TDD is a core discipline in Superpowers:

  1. RED - Write a failing test first
  2. GREEN - Write the minimum code to pass
  3. REFACTOR - Improve the code without changing behavior

Key points

  • ✅ Tests always come first
  • ✅ Iterate in small steps
  • ✅ Tests are documentation
  • ✅ Coverage is a result, not the goal

Benefits of TDD

  • 📉 Bugs drop by 60-80%
  • 📈 Refactoring confidence improves
  • 📚 Living documentation is generated automatically
  • 🧠 Better design, because you are forced to think through interfaces

Series navigation:

Reference Resources

Last updated on Mar 26, 2026 00:00 UTC