TDD Workflow Skill
Overview
Test-Driven Development (TDD) skill implementing the red-green-refactor cycle, test-first development, and comprehensive testing strategies across multiple languages and frameworks.
Capabilities
1. TDD Cycle Implementation
- Red: Write failing test first
- Green: Write minimal code to pass
- Refactor: Improve code while keeping tests green
- Test coverage tracking
- Continuous test execution
2. Testing Strategies
- Unit testing
- Integration testing
- End-to-end testing
- Test doubles (mocks, stubs, spies)
- Parameterized testing
3. Multi-Language Support
- Python: pytest, unittest
- JavaScript: Jest, Mocha, Vitest
- Java: JUnit, TestNG, Mockito
- Go: testing package
- Ruby: RSpec
- .NET: xUnit, NUnit
TDD Principles
The Three Laws of TDD
- Don't write production code until you have a failing test
- Don't write more of a test than is sufficient to fail
- Don't write more production code than is sufficient to pass the test
Red-Green-Refactor Cycle
1. RED: Write a failing test
↓
2. GREEN: Make it pass (simplest way)
↓
3. REFACTOR: Improve the code
↓
Repeat
Python TDD with pytest
Setup
pip install pytest pytest-cov pytest-watch
Example Workflow
Step 1: RED - Write failing test
# test_calculator.py
import pytest
from calculator import Calculator
def test_add_two_numbers():
calc = Calculator()
result = calc.add(2, 3)
assert result == 5 # This will fail - Calculator doesn't exist yet!
Run test:
pytest test_calculator.py
# ❌ FAIL: ModuleNotFoundError: No module named 'calculator'
Step 2: GREEN - Make it pass
# calculator.py
class Calculator:
def add(self, a, b):
return a + b
Run test:
pytest test_calculator.py
# ✅ PASS: 1 passed
Step 3: REFACTOR - Improve code
# calculator.py
class Calculator:
def add(self, a: int, b: int) -> int:
"""Add two numbers and return the result."""
return a + b
Run test again:
pytest test_calculator.py
# ✅ PASS: 1 passed (still works!)
Advanced Testing
Parameterized Tests
import pytest
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300)
])
def test_add_multiple_cases(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
Test Fixtures
@pytest.fixture
def calculator():
return Calculator()
def test_add(calculator):
assert calculator.add(2, 3) == 5
def test_subtract(calculator):
assert calculator.subtract(5, 3) == 2
Mocking
from unittest.mock import Mock, patch
def test_api_call():
with patch('requests.get') as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'data': 'test'}
result = fetch_data('http://api.example.com')
assert result == {'data': 'test'}
JavaScript TDD with Jest
Setup
npm install --save-dev jest @types/jest
Example Workflow
Step 1: RED
// calculator.test.js
const Calculator = require('./calculator');
describe('Calculator', () => {
test('adds two numbers', () => {
const calc = new Calculator();
expect(calc.add(2, 3)).toBe(5); // FAIL: Calculator not defined
});
});
Step 2: GREEN
// calculator.js
class Calculator {
add(a, b) {
return a + b;
}
}
module.exports = Calculator;
Step 3: REFACTOR
// calculator.js
class Calculator {
/**
* Adds two numbers
* @param {number} a - First number
* @param {number} b - Second number
* @returns {number} Sum of a and b
*/
add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Both arguments must be numbers');
}
return a + b;
}
}
module.exports = Calculator;
Jest Features
Test Coverage
jest --coverage
Watch Mode
jest --watch
Mocking
// Mock API call
jest.mock('./api');
const api = require('./api');
test('fetches data', async () => {
api.fetchData.mockResolvedValue({ data: 'test' });
const result = await getData();
expect(result.data).toBe('test');
});
Java TDD with JUnit 5
Setup (Maven)
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Example Workflow
Step 1: RED
// CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void testAdd() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3)); // FAIL: Calculator not found
}
}
Step 2: GREEN
// Calculator.java
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
Step 3: REFACTOR
// Calculator.java
public class Calculator {
/**
* Adds two integers
* @param a first integer
* @param b second integer
* @return sum of a and b
*/
public int add(int a, int b) {
return Math.addExact(a, b); // Throws on overflow
}
}
JUnit 5 Features
Parameterized Tests
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"0, 0, 0",
"-1, 1, 0"
})
void testAddMultipleCases(int a, int b, int expected) {
Calculator calc = new Calculator();
assertEquals(expected, calc.add(a, b));
}
Test Lifecycle
@BeforeEach
void setUp() {
calc = new Calculator();
}
@AfterEach
void tearDown() {
calc = null;
}
Mockito Integration
@ExtendWith(MockitoExtension.class)
class ServiceTest {
@Mock
private Repository repository;
@InjectMocks
private Service service;
@Test
void testServiceMethod() {
when(repository.findById(1)).thenReturn(Optional.of(entity));
Result result = service.process(1);
assertEquals("expected", result.getValue());
}
}
TDD Best Practices
1. Write Tests First
❌ BAD: Write code, then test
✅ GOOD: Write test, then code
2. Small Steps
# Don't try to implement everything at once
def test_calculator():
calc = Calculator()
assert calc.add(2, 3) == 5 # Start simple
# Later add more tests:
# - multiply
# - divide
# - error handling
3. One Test at a Time
✅ Write one test
✅ Make it pass
✅ Refactor
❌ Don't write multiple failing tests
4. Test Behavior, Not Implementation
# ❌ BAD: Testing implementation details
def test_uses_dictionary():
cache = Cache()
assert isinstance(cache._storage, dict)
# ✅ GOOD: Testing behavior
def test_stores_and_retrieves_value():
cache = Cache()
cache.set('key', 'value')
assert cache.get('key') == 'value'
5. Fast Tests
# ✅ Fast: In-memory operations
def test_calculation():
result = calculate(2, 3)
assert result == 5
# ❌ Slow: Database/network operations (use mocks)
def test_database():
# Use test database or mock instead
pass
Test Coverage
Aim for High Coverage
# Python
pytest --cov=src --cov-report=html
# JavaScript
jest --coverage
# Java
mvn test jacoco:report
Coverage Guidelines
- 80-90%: Good target for most projects
- 100%: Not always necessary or practical
- Focus: Critical business logic > boilerplate
Coverage Report Example
Name Stmts Miss Cover
---------------------------------------
calculator.py 10 0 100%
database.py 50 5 90%
utils.py 20 8 60%
---------------------------------------
TOTAL 80 13 84%
Continuous Testing
pytest-watch
# Auto-run tests on file changes
pytest-watch
Jest watch mode
# Interactive test runner
jest --watch
Gradle continuous testing
# Java continuous testing
gradle test --continuous
Integration Scripts
tdd_helper.sh
TDD workflow automation:
#!/bin/bash
# TDD workflow helper
ACTION=$1
case $ACTION in
"red")
echo "🔴 RED: Writing failing test..."
echo "1. Write a test that fails"
echo "2. Run: npm test / pytest / mvn test"
;;
"green")
echo "🟢 GREEN: Making test pass..."
echo "1. Write minimal code to pass"
echo "2. Run tests again"
;;
"refactor")
echo "🔵 REFACTOR: Improving code..."
echo "1. Improve code quality"
echo "2. Keep tests green"
echo "3. Run tests continuously"
;;
"watch")
echo "👀 Starting watch mode..."
if [ -f "package.json" ]; then
jest --watch
elif [ -f "pytest.ini" ] || [ -f "setup.py" ]; then
pytest-watch
else
echo "No test framework detected"
fi
;;
*)
echo "Usage: $0 {red|green|refactor|watch}"
;;
esac
test_coverage_checker.py
Enforce coverage standards:
#!/usr/bin/env python3
import json
import sys
def check_coverage(coverage_json, threshold=80):
"""Check if test coverage meets threshold"""
with open(coverage_json) as f:
data = json.load(f)
total_coverage = data['totals']['percent_covered']
print(f"Test Coverage: {total_coverage:.1f}%")
if total_coverage < threshold:
print(f"❌ FAIL: Coverage below {threshold}%")
sys.exit(1)
else:
print(f"✅ PASS: Coverage meets {threshold}% threshold")
sys.exit(0)
if __name__ == '__main__':
check_coverage('coverage.json', threshold=80)
Common TDD Patterns
1. Arrange-Act-Assert (AAA)
def test_user_creation():
# Arrange
username = "testuser"
email = "test@example.com"
# Act
user = User.create(username, email)
# Assert
assert user.username == username
assert user.email == email
2. Given-When-Then (BDD style)
describe('User Login', () => {
it('should authenticate valid credentials', () => {
// Given
const user = createUser('john', 'password123');
// When
const result = authenticate('john', 'password123');
// Then
expect(result.success).toBe(true);
});
});
3. Test Doubles
# Dummy: Passed but never used
def test_with_dummy():
dummy = DummyLogger()
service = Service(dummy)
# Stub: Returns canned responses
def test_with_stub():
stub = StubDatabase()
stub.set_return_value([User('test')])
# Mock: Verifies interactions
def test_with_mock():
mock = Mock()
service.notify(mock)
mock.send.assert_called_once()
Requirements
# Python
pip install pytest pytest-cov pytest-watch pytest-mock
# JavaScript
npm install --save-dev jest @testing-library/react
# Java
# Add JUnit 5 + Mockito to Maven/Gradle
# Coverage tools
# Python: coverage.py (included with pytest-cov)
# JavaScript: built into Jest
# Java: JaCoCo
Metrics to Track
- Test coverage: > 80%
- Test execution time: < 1 minute for unit tests
- Test count: Growing with features
- Failure rate: Quick to identify and fix
- Refactoring safety: Tests enable confident refactoring