ワンクリックで
reverse-engineer-tdd-tests
Apply TDD principles retrospectively to existing code by writing tests "as if" they were written first
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Apply TDD principles retrospectively to existing code by writing tests "as if" they were written first
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | reverse-engineer-tdd-tests |
| description | Apply TDD principles retrospectively to existing code by writing tests "as if" they were written first |
When implementation already exists, you cannot follow strict Red-Green-Refactor TDD. However, you can apply TDD principles retrospectively by writing tests "as if" they were written before implementation.
Core principle: Write focused, behavior-driven tests that would have guided the implementation if written first.
Language-agnostic: This approach works for any language (Python, TypeScript, Java, Go, etc.)
| Aspect | True TDD | Reverse Engineering TDD |
|---|---|---|
| Order | Test → Implementation | Implementation → Test |
| Red Phase | Watch test fail | Verify test passes (implementation exists) |
| Discovery | Tests drive design | Tests document design |
| Confidence | High (saw it fail) | Medium (must verify thoroughly) |
1. ANALYZE → 2. WRITE → 3. VERIFY → 4. REFACTOR → Repeat
Read implementation to identify discrete, testable behaviors.
Good Analysis:
# Analyzing processPayment():
# 1. Returns error when payment gateway unavailable
# 2. Returns error when validation fails
# 3. Returns error when exception raised
# 4. Returns success and stores transaction on success
# 5. Skips notification when --silent flag set
# 6. Logs retry message when retry attempted
Bad Analysis:
# Analyzing processPayment():
# 1. Tests the function works
# 2. Tests error handling
# 3. Tests success case
Too vague, not actionable
Write one test for one behavior, as if you were writing it before implementation.
Requirements:
Good Test (Python):
def test_gateway_unavailable_returns_error():
"""Test that unavailable payment gateway returns error.
GIVEN payment gateway is unavailable
WHEN processPayment executes
THEN returns error code and logs failure.
"""
with patch("payment.gateway.connect") as mock_connect:
mock_connect.return_value = None
result = processPayment(amount=100, currency="USD")
assert result.status == "error"
assert "gateway unavailable" in result.message
Good Test (TypeScript):
test('gateway unavailable returns error', async () => {
// GIVEN payment gateway is unavailable
const mockGateway = jest.fn().mockResolvedValue(null);
// WHEN processPayment executes
const result = await processPayment({
amount: 100,
currency: 'USD',
gateway: mockGateway
});
// THEN returns error
expect(result.status).toBe('error');
expect(result.message).toContain('gateway unavailable');
});
Bad Test:
def test_payment():
"""Test payment processing."""
# Tests multiple behaviors at once
# Vague name and docstring
# Over-mocked
Run the test and verify it passes because implementation exists.
Python:
pytest tests/test_payment.py::test_gateway_unavailable_returns_error -v
TypeScript:
npm test -- payment.test.ts -t "gateway unavailable"
Java:
mvn test -Dtest=PaymentTest#testGatewayUnavailableReturnsError
Confirm:
Test fails? Determine the cause:
Bug in Implementation ✅ Fix it
Missing Feature ❌ Don't implement it
Test is Wrong ✅ Fix the test
Mocking is Incorrect ✅ Fix the mocks
Critical Distinction:
| Scenario | Action | Reason |
|---|---|---|
| Bug: Code crashes on null input | ✅ Fix bug | Existing code should handle this |
| Bug: Returns wrong error code | ✅ Fix bug | Existing code has incorrect behavior |
| Missing: No support for new format | ❌ Don't add feature | Not part of existing behavior |
| Missing: Entire code path doesn't exist | ❌ Don't implement | New feature, not reverse engineering |
When in doubt: Ask yourself "Did the original developer intend this behavior?" If yes, it's a bug to fix. If no, it's a feature to skip.
After test passes, refactor for clarity:
Keep test passing throughout refactoring.
Core Principle: Mock at system boundaries, test real behavior including file operations and data transformations.
Mock external dependencies that are:
Examples by Category:
1. External APIs / Services
# Python
@patch("module.api_client.fetch_data")
@patch("module.payment_gateway.process")
// TypeScript
jest.mock('./apiClient', () => ({
fetchData: jest.fn(),
processPayment: jest.fn()
}));
2. Database Operations
# Python
@patch("module.database.save")
@patch("module.database.query")
// Java
@Mock
private DatabaseRepository repository;
3. Network Operations
# Python
@patch("module.http_client.get")
@patch("module.download_file")
// TypeScript
jest.mock('axios');
4. Time-Dependent Operations
# Python
@patch("module.datetime.now")
@patch("module.time.sleep")
// TypeScript
jest.useFakeTimers();
Why: External dependencies are slow, require services, and are tested elsewhere.
NEVER mock these:
1. File Read/Write Operations ✅
# Python - DON'T mock file operations
# DO use temporary directories
def test_create_report_success(tmp_path):
output_file = tmp_path / "report.md"
create_report(output_file)
assert output_file.exists()
assert "expected content" in output_file.read_text()
// TypeScript - DON'T mock fs
// DO use temporary directories
test('creates report file', () => {
const tmpDir = fs.mkdtempSync('/tmp/test-');
const outputFile = path.join(tmpDir, 'report.md');
createReport(outputFile);
expect(fs.existsSync(outputFile)).toBe(true);
});
2. Argument Processing ✅
# DON'T mock argument objects
# DO create real objects with test data
args = {"instance_id": "test", "build_id": "123"}
3. Data Transformation ✅
# DON'T mock transformation functions
# DO let them run and verify output
result = formatData(input_data)
assert result == expected_output
4. Logging ✅
# DON'T mock logger
# DO let it log (fast, part of observable behavior)
# Optionally capture logs to verify messages
5. Business Logic ✅
# DON'T mock calculation or validation functions
# DO test them directly with real inputs
result = calculateTotal(items)
assert result == expected_total
When mocking functions that interact with files, create real files in temporary locations:
Python Example:
def test_process_with_real_files(tmp_path):
"""Test processing with real file operations."""
# Setup: Create real directory structure
data_dir = tmp_path / "data"
data_dir.mkdir(parents=True)
with patch("module.download_data") as mock_download:
# Mock returns path to real temp directory
mock_download.return_value = data_dir / "data.json"
# Mock creates real file with test data
def create_file(*args):
file_path = data_dir / "result.txt"
file_path.write_text("Test results")
return {"success": True}
with patch("module.external_process") as mock_process:
mock_process.side_effect = create_file
# Execute - performs real file I/O
result = process_data(data_dir)
# Verify real file was created
assert result == 0
assert (data_dir / "result.txt").exists()
TypeScript Example:
test('processes with real files', () => {
const tmpDir = fs.mkdtempSync('/tmp/test-');
// Mock external download but create real file
const mockDownload = jest.fn().mockImplementation(() => {
const filePath = path.join(tmpDir, 'data.json');
fs.writeFileSync(filePath, JSON.stringify({test: 'data'}));
return filePath;
});
const result = processData(tmpDir, mockDownload);
expect(result.success).toBe(true);
expect(fs.existsSync(path.join(tmpDir, 'result.txt'))).toBe(true);
});
Slow tests indicate over-mocking or testing wrong things.
tests/cli/test_<module>.py
Pattern: test_<behavior>_<condition>_<expected_result>
Examples across languages:
Python:
def test_gateway_unavailable_returns_error():
def test_cache_hit_skips_download():
def test_successful_payment_stores_transaction():
TypeScript:
test('gateway unavailable returns error', ...)
test('cache hit skips download', ...)
test('successful payment stores transaction', ...)
Java:
@Test
public void testGatewayUnavailableReturnsError() { ... }
@Test
public void testCacheHitSkipsDownload() { ... }
Go:
func TestGatewayUnavailableReturnsError(t *testing.T) { ... }
func TestCacheHitSkipsDownload(t *testing.T) { ... }
Python:
def test_behavior():
"""Test description.
GIVEN preconditions
WHEN action occurs
THEN expected result.
"""
# Arrange: Setup mocks and test data
with patch("module.external_function") as mock:
mock.return_value = test_value
test_input = {"key": "value"}
# Act: Execute the behavior
result = function_under_test(test_input)
# Assert: Verify expected outcome
assert result == expected
mock.assert_called_once()
TypeScript:
test('behavior description', () => {
// Arrange
const mockFunction = jest.fn().mockResolvedValue(testValue);
const testInput = { key: 'value' };
// Act
const result = functionUnderTest(testInput, mockFunction);
// Assert
expect(result).toBe(expected);
expect(mockFunction).toHaveBeenCalledTimes(1);
});
Java:
@Test
public void testBehavior() {
// Arrange
when(mockService.externalFunction()).thenReturn(testValue);
TestInput input = new TestInput("value");
// Act
Result result = functionUnderTest(input);
// Assert
assertEquals(expected, result);
verify(mockService, times(1)).externalFunction();
}
# BAD: Tests how code works internally
def test_uses_correct_algorithm():
with mock("module.internal_helper") as mock:
function()
verify(mock).called_with(specific_args)
# GOOD: Tests what code does
def test_returns_sorted_results():
result = function(unsorted_data)
assert result == sorted_data
# BAD: Mocks everything including file operations
with mock("file_system.open"), \
mock("file_system.exists"), \
mock("file_system.mkdir"):
# Test doesn't verify real file handling
# GOOD: Only mocks external boundaries
def test_with_real_files(tmp_dir):
with mock("module.download_from_api") as mock:
mock.return_value = tmp_dir / "file.dat"
# Real file operations happen
# BAD: Tests multiple things
def test_function_works():
# Tests success, error handling, and caching
assert result1 == success
assert result2 == error
assert cache_used
# GOOD: Focused tests
def test_success_case():
assert result == success
def test_error_case():
assert result == error
def test_cache_hit_skips_operation():
assert not operation_called
Before marking reverse engineering complete:
Implementation to Test:
def generate_report(data_source, output_dir, format="markdown"):
"""Generate report from data source."""
try:
# Fetch data from external source
data = fetch_data(data_source)
if not data:
logger.warning("No data found")
return 0
# Transform data to desired format
content = format_data(data, format)
# Write to file
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
output_file = output_path / f"report.{format}"
with open(output_file, "w") as f:
f.write(content)
logger.info(f"Report created: {output_file}")
return 0
except ValueError as e:
logger.error(f"Error: {e}")
return 1
Step 1: ANALYZE Behaviors
Step 2: WRITE Tests
def test_generate_report_success(tmp_path):
"""Test successful report generation.
GIVEN fetch_data returns valid data
WHEN generate_report executes
THEN report file is created and function returns 0.
"""
with patch("module.fetch_data") as mock_fetch, \
patch("module.format_data") as mock_format:
mock_fetch.return_value = {"key": "value"}
mock_format.return_value = "# Report\n\nContent..."
result = generate_report(
data_source="test_source",
output_dir=str(tmp_path),
format="markdown"
)
assert result == 0
output_file = tmp_path / "report.markdown"
assert output_file.exists()
assert "# Report" in output_file.read_text()
def test_generate_report_no_data(tmp_path):
"""Test report generation with no data.
GIVEN fetch_data returns empty result
WHEN generate_report executes
THEN warning is logged and function returns 0.
"""
with patch("module.fetch_data") as mock_fetch:
mock_fetch.return_value = None
result = generate_report(
data_source="test_source",
output_dir=str(tmp_path)
)
assert result == 0
def test_generate_report_value_error(tmp_path):
"""Test report generation handles ValueError.
GIVEN fetch_data raises ValueError
WHEN generate_report executes
THEN error is logged and function returns 1.
"""
with patch("module.fetch_data") as mock_fetch:
mock_fetch.side_effect = ValueError("Test error")
result = generate_report(
data_source="test_source",
output_dir=str(tmp_path)
)
assert result == 1
Step 3: VERIFY
pytest tests/test_report.py::test_generate_report_success -v
pytest tests/test_report.py::test_generate_report_no_data -v
pytest tests/test_report.py::test_generate_report_value_error -v
All pass ✅
Step 4: REFACTOR Tests are clear and focused. No refactoring needed.
1. Write failing test
2. Watch it fail (proves test works)
3. Write minimal code to pass
4. Watch it pass
5. Refactor
Confidence: High - saw test fail and pass
1. Analyze existing implementation
2. Write test as if it were first
3. Verify test passes (implementation exists)
4. Refactor test for clarity
Confidence: Medium - must verify thoroughly
| Situation | Approach |
|---|---|
| New feature | True TDD (test-first) |
| Bug fix | True TDD (test-first) |
| Existing untested code | Reverse Engineering TDD |
| Refactoring tests | Reverse Engineering TDD |
| Legacy code | Reverse Engineering TDD |
Reverse Engineering TDD is not an excuse to skip true TDD. It's a pragmatic approach for:
For new code, always use true TDD (test-first).
The goal is to write tests that would have guided the implementation if written first, even though they weren't.