Red-Green-Refactor Methodology
You are following the RED-GREEN-REFACTOR cycle for test-driven development. Every new feature, bug fix, or behavior change starts with a failing test.
The Cycle
1. RED Phase β Write a Failing Test
- Understand the requirement β what specific behavior must exist?
- Write one test asserting that behavior
- Run the test β it MUST fail (red)
- Verify the failure reason β not a syntax error, but a missing implementation
The test should be focused on ONE behavior, named descriptively, and use clear assertions.
Executable example (Jest):
// calculateTotal.test.js
const { calculateTotal } = require('./calculateTotal');
describe('calculateTotal', () => {
it('should apply 10% discount when total exceeds 100', () => {
const items = [{ price: 60 }, { price: 60 }]; // total = 120
expect(calculateTotal(items)).toBe(108); // 120 * 0.90
});
});Running this now produces: Cannot find module './calculateTotal' β correct RED state.
2. GREEN Phase β Make the Test Pass
Write the minimum code needed to pass the test. Don't add anything extra.
// calculateTotal.js
function calculateTotal(items) {
const total = items.reduce((sum, item) => sum + item.price, 0);
return total > 100 ? total * 0.9 : total;
}
module.exports = { calculateTotal };Run the test β it passes. GREEN achieved. Stop here; resist adding more logic.
3. REFACTOR Phase β Improve the Code
With a passing test as your safety net, clean up the implementation. Run tests after every change.
// calculateTotal.js β refactored for clarity
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.9;
function calculateTotal(items) {
const subtotal = items.reduce((sum, { price }) => sum + price, 0);
return subtotal > DISCOUNT_THRESHOLD ? subtotal * DISCOUNT_RATE : subtotal;
}
module.exports = { calculateTotal };Test still passes β GREEN maintained. Constants now communicate intent.
End-to-End Example: Adding a New Behavior
Next requirement: apply a 15% discount when total exceeds 200.
RED β write the failing test first:
it('should apply 15% discount when total exceeds 200', () => {
const items = [{ price: 110 }, { price: 110 }]; // total = 220
expect(calculateTotal(items)).toBe(187); // 220 * 0.85
});GREEN β extend the implementation minimally:
function calculateTotal(items) {
const subtotal = items.reduce((sum, { price }) => sum + price, 0);
if (subtotal > 200) return subtotal * 0.85;
if (subtotal > 100) return subtotal * 0.9;
return subtotal;
}REFACTOR β remove duplication with a tiered structure:
const DISCOUNT_TIERS = [
{ threshold: 200, rate: 0.85 },
{ threshold: 100, rate: 0.9 },
];
function calculateTotal(items) {
const subtotal = items.reduce((sum, { price }) => sum + price, 0);
const tier = DISCOUNT_TIERS.find(({ threshold }) => subtotal > threshold);
return tier ? subtotal * tier.rate : subtotal;
}Both tests pass β ready for the next cycle.
Workflow Steps
- Create or open the test file first
- Write ONE failing test for the smallest testable unit
- Implement minimally β just enough to pass
- Refactor if needed β while tests stay green
- Repeat for the next behavior
Decision Points
Write a new test when:
- Adding a new feature or behavior
- Fixing a bug (test the bug first, then fix it)
- Handling an edge case discovered during implementation
Don't write a test when:
- Pure refactoring (existing tests already cover the behavior)
- Non-functional changes (formatting, comments)
- Third-party library internals
Verification Checklist
- All new code has corresponding tests
- Tests fail when the feature is removed
- Tests pass consistently (not flaky)
- Code has been refactored for clarity
- No unnecessary code was added
Common Mistakes to Avoid
- Writing tests after code β defeats the design benefit of TDD
- Writing multiple tests at once β one test drives one change
- Passing tests with hacks β the test should drive good design
- Skipping the refactor phase β technical debt accumulates
- Testing implementation details β test behavior, not internals
Integration with Other Skills
- test-patterns: Patterns for structuring tests
- anti-patterns: Common testing mistakes to avoid
- debugging/root-cause-analysis: When tests reveal unexpected failures