| name | tdd-workflow |
| description | Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests. |
Test-Driven Development Workflow
This skill ensures all code development follows TDD principles with comprehensive test coverage.
When to Activate
- Writing new features or functionality
- Fixing bugs or issues
- Refactoring existing code
- Adding API endpoints
- Creating new services or classes
Core Principles
1. Tests BEFORE Code
ALWAYS write tests first, then implement code to make tests pass.
2. Coverage Requirements
- Changed-lines coverage: >= 90%
- Global coverage: >= 85%
- Critical paths: 100%
- Error paths: >= 95%
3. Test Types
Unit Tests (~70%)
- Individual functions and utilities
- Class methods
- Pure functions
- Helpers and utilities
Integration Tests (~20%)
- API endpoints
- Database operations
- Service interactions
- External API calls
E2E Tests (~10% - Playwright)
- Critical user flows
- Complete workflows
- Browser automation
- UI interactions
TDD Workflow Steps
Step 1: Write User Stories
As a [role], I want to [action], so that [benefit]
Example:
As a recruiter, I want to match candidates to jobs semantically,
so that I can find relevant candidates even without exact skill matches.
Step 2: Generate Test Cases
For each user story, create comprehensive test cases:
import pytest
from unittest.mock import Mock, patch
class TestSemanticSearch:
"""Tests for semantic search functionality."""
def test_returns_relevant_candidates_for_query(self, search_service):
"""Search returns candidates matching semantic query."""
results = search_service.search("Python developer")
assert len(results) > 0
assert all(r.relevance_score > 0.7 for r in results)
def test_handles_empty_query_gracefully(self, search_service):
"""Empty query returns empty results without error."""
results = search_service.search("")
assert results == []
def test_falls_back_to_keyword_search_when_embedding_unavailable(
self, search_service, mock_embedding_service
):
"""Falls back to keyword search when embedding fails."""
mock_embedding_service.generate.side_effect = ConnectionError()
results = search_service.search("Python")
assert len(results) > 0
def test_sorts_results_by_similarity_score(self, search_service):
"""Results are sorted by descending similarity score."""
results = search_service.search("data scientist")
scores = [r.relevance_score for r in results]
assert scores == sorted(scores, reverse=True)
Step 3: Run Tests (They Should Fail)
pytest tests/ -v
Step 4: Implement Code
Write minimal code to make tests pass:
class SemanticSearchService:
"""Service for semantic candidate search."""
def __init__(self, embedding_service: EmbeddingService, repository: CandidateRepository):
self._embedding_service = embedding_service
self._repository = repository
def search(self, query: str) -> list[SearchResult]:
"""Search candidates semantically."""
if not query.strip():
return []
try:
embedding = self._embedding_service.generate(query)
candidates = self._repository.find_by_embedding(embedding)
except ConnectionError:
candidates = self._repository.find_by_keyword(query)
return sorted(candidates, key=lambda c: c.relevance_score, reverse=True)
Step 5: Run Tests Again
pytest tests/ -v
Step 6: Refactor
Improve code quality while keeping tests green:
- Remove duplication
- Improve naming
- Optimize performance
- Enhance readability
Step 7: Verify Coverage
pytest --cov=src --cov-report=term-missing --cov-fail-under=85
Testing Patterns
Unit Test Pattern (pytest)
import pytest
from src.services.candidate_scorer import CandidateScorer
class TestCandidateScorer:
"""Unit tests for CandidateScorer."""
@pytest.fixture
def scorer(self):
"""Create scorer instance for tests."""
return CandidateScorer()
def test_scores_perfect_match_as_1(self, scorer):
"""Perfect skill match scores 1.0."""
candidate_skills = {"Python", "FastAPI", "PostgreSQL"}
job_skills = {"Python", "FastAPI", "PostgreSQL"}
score = scorer.calculate_skill_match(candidate_skills, job_skills)
assert score == 1.0
def test_scores_no_match_as_0(self, scorer):
"""No skill overlap scores 0.0."""
candidate_skills = {"Java", "Spring"}
job_skills = {"Python", "FastAPI"}
score = scorer.calculate_skill_match(candidate_skills, job_skills)
assert score == 0.0
def test_scores_partial_match_proportionally(self, scorer):
"""Partial match scores proportionally."""
candidate_skills = {"Python", "Java"}
job_skills = {"Python", "FastAPI", "PostgreSQL"}
score = scorer.calculate_skill_match(candidate_skills, job_skills)
assert 0.3 <= score <= 0.4
API Integration Test Pattern (FastAPI)
import pytest
from httpx import AsyncClient
from fastapi import status
from src.main import app
@pytest.mark.asyncio
class TestCandidatesAPI:
"""Integration tests for candidates API."""
async def test_search_candidates_returns_results(self, client: AsyncClient):
"""GET /api/candidates/search returns matching candidates."""
response = await client.get("/api/candidates/search", params={"q": "Python"})
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
assert isinstance(data["data"], list)
async def test_search_validates_query_parameter(self, client: AsyncClient):
"""GET /api/candidates/search validates query params."""
response = await client.get("/api/candidates/search", params={"limit": "invalid"})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_search_handles_database_errors_gracefully(
self, client: AsyncClient, mock_db_failure
):
"""Database errors return 500 with generic message."""
response = await client.get("/api/candidates/search", params={"q": "test"})
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
data = response.json()
assert "detail" in data
assert "database" not in data["detail"].lower()
E2E Test Pattern (Playwright)
import pytest
from playwright.sync_api import Page, expect
class TestCandidateSearchFlow:
"""E2E tests for candidate search user flow."""
def test_user_can_search_and_view_candidate(self, page: Page):
"""Complete user journey: search -> view candidate."""
page.goto("/candidates")
expect(page.locator("h1")).to_contain_text("Candidates")
page.fill('input[placeholder="Search candidates"]', "Python developer")
page.wait_for_response(
lambda resp: "/api/candidates/search" in resp.url and resp.status == 200
)
results = page.locator('[data-testid="candidate-card"]')
expect(results.first).to_be_visible()
results.first.click()
expect(page).to_have_url(r"/candidates/[a-z0-9-]+")
expect(page.locator('[data-testid="candidate-name"]')).to_be_visible()
def test_search_with_no_results_shows_empty_state(self, page: Page):
"""Empty search results show appropriate message."""
page.goto("/candidates")
page.fill('input[placeholder="Search candidates"]', "xyznonexistent123456")
page.wait_for_load_state("networkidle")
no_results = page.locator('[data-testid="no-results"]')
expect(no_results).to_be_visible()
expect(no_results).to_contain_text("No candidates found")
Test File Organization
tests/
├── unit/
│ ├── services/
│ │ ├── test_candidate_scorer.py
│ │ └── test_embedding_service.py
│ ├── repositories/
│ │ └── test_candidate_repository.py
│ └── utils/
│ └── test_skill_normalizer.py
├── integration/
│ ├── api/
│ │ ├── test_candidates_api.py
│ │ └── test_jobs_api.py
│ └── database/
│ └── test_candidate_queries.py
├── e2e/
│ ├── test_candidate_search.py
│ ├── test_job_matching.py
│ └── test_export_results.py
├── fixtures/
│ ├── candidates.py
│ └── jobs.py
└── conftest.py
Mocking External Services
Database Mock
import pytest
from unittest.mock import AsyncMock
@pytest.fixture
def mock_db_session():
"""Mock database session."""
session = AsyncMock()
session.execute.return_value.scalars.return_value.all.return_value = [
Candidate(id=1, name="John Doe", skills=["Python", "FastAPI"])
]
return session
Embedding Service Mock
import pytest
from unittest.mock import Mock
import numpy as np
@pytest.fixture
def mock_embedding_service():
"""Mock embedding service."""
service = Mock()
service.generate.return_value = np.random.rand(1536).tolist()
return service
External API Mock
import pytest
from unittest.mock import patch
@pytest.fixture
def mock_esco_api():
"""Mock ESCO API responses."""
with patch("src.integrations.esco_client.ESCOClient") as mock:
mock.return_value.get_skills.return_value = [
{"uri": "http://esco/skill/1", "label": "Python programming"}
]
yield mock
Test Coverage Configuration
pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
addopts = [
"--cov=src",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-fail-under=85",
]
[tool.coverage.run]
branch = true
source = ["src"]
omit = ["*/__init__.py", "*/migrations/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
Common Testing Mistakes to Avoid
Testing Implementation Details
def test_private_method(self):
assert obj._internal_state == expected
def test_public_behavior(self):
assert obj.get_result() == expected
Multiple Behaviors Per Test
def test_everything(self):
assert validator.is_valid(data1)
assert not validator.is_valid(data2)
assert validator.errors == expected
def test_accepts_valid_data(self):
assert validator.is_valid(valid_data)
def test_rejects_invalid_data(self):
assert not validator.is_valid(invalid_data)
Excessive Mocking
def test_with_all_mocks(self):
mock_db = Mock()
mock_cache = Mock()
mock_validator = Mock()
service = Service(mock_db, mock_cache, mock_validator)
def test_with_minimal_mocks(self):
real_validator = Validator()
real_cache = InMemoryCache()
mock_db = Mock()
service = Service(mock_db, real_cache, real_validator)
Brittle Selectors (E2E)
await page.click(".css-class-xyz")
await page.click('button:has-text("Submit")')
await page.click('[data-testid="submit-button"]')
No Test Isolation
def test_creates_user(self):
self.user = create_user()
def test_updates_same_user(self):
update_user(self.user)
def test_creates_user(self):
user = create_test_user()
def test_updates_user(self):
user = create_test_user()
update_user(user)
Continuous Testing
Watch Mode During Development
pytest-watch
Pre-Commit Hook
repos:
- repo: local
hooks:
- id: pytest
name: pytest
entry: pytest tests/unit -q
language: system
pass_filenames: false
always_run: true
CI/CD Integration
- name: Run Tests
run: pytest --cov=src --cov-report=xml
- name: Upload Coverage
uses: codecov/codecov-action@v4
with:
files: coverage.xml
Best Practices
- Write Tests First - Always TDD
- One Assert Per Test - Focus on single behavior
- Descriptive Test Names - Explain what's tested
- Arrange-Act-Assert - Clear test structure
- Mock External Dependencies - Isolate unit tests
- Test Edge Cases - None, empty, boundary values
- Test Error Paths - Not just happy paths
- Keep Tests Fast - Unit tests < 100ms each
- Clean Up After Tests - No side effects
- Review Coverage Reports - Identify gaps
Success Metrics
- Changed-lines coverage >= 90%
- Global coverage >= 85%
- All tests passing (green)
- No skipped or disabled tests
- Fast test execution (unit < 100ms, integration < 1s, E2E < 10s)
- E2E tests cover critical user flows
- Tests catch bugs before production
Remember: Tests are not optional. They are the safety net that enables confident refactoring, rapid development, and production reliability. One vulnerability can compromise the entire platform.