Red-Green-Refactor
Red-Green-Refactor
Overview
Red-Green-Refactor is the core TDD cycle that guides development through three distinct phases: writing a failing test (Red), making it pass (Green), and improving the design (Refactor). This rhythm ensures code quality, maintainability, and comprehensive test coverage while providing rapid feedback on both functionality and design.
The discipline of following Red-Green-Refactor prevents common pitfalls like untested code, over-engineering, and technical debt accumulation.
Key Concepts
The Three Phases
┌──────────────────────────────────────────────────────┐
│ Red-Green-Refactor Cycle │
├──────────────────────────────────────────────────────┤
│ │
│ 🔴 RED Phase │
│ • Write a failing test │
│ • Test should fail for the right reason │
│ • Verify test execution │
│ │
│ ↓ │
│ │
│ 🟢 GREEN Phase │
│ • Write minimal code to pass │
│ • No refactoring yet │
│ • Get to green quickly │
│ │
│ ↓ │
│ │
│ 🔵 REFACTOR Phase │
│ • Improve code design │
│ • Eliminate duplication │
│ • Tests must stay green │
│ │
│ ↓ │
│ │
│ Repeat for next behavior │
│ │
└──────────────────────────────────────────────────────┘
Phase Rules
RED
- Test must fail (compilation error or assertion failure)
- Failure confirms test actually tests something
- Only write enough test code to fail
GREEN
- Make test pass with simplest possible code
- Hardcoding values is acceptable initially
- Don’t write more production code than needed
REFACTOR
- Clean up both test and production code
- Remove duplication
- Improve names and structure
- Tests must remain green throughout
Best Practices
1. Keep Cycles Short
Each Red-Green-Refactor cycle should take 1-10 minutes. Shorter cycles provide faster feedback.
2. Red First, Always
Never skip seeing the test fail. This confirms the test is testing something real.
3. Smallest Step in Green
Resist the temptation to add “just one more feature” while making the test green.
4. Refactor Both Test and Production
Clean code applies to tests too. Keep them readable and maintainable.
5. One Concept Per Cycle
Focus on one behavior or edge case per Red-Green-Refactor iteration.
Code Examples
Example 1: Basic Red-Green-Refactor Cycle
// 🔴 RED: Write failing test
@Test
void shouldReturnEmptyListWhenNoOrders() {
OrderRepository repository = new OrderRepository();
List<Order> orders = repository.findAll();
assertThat(orders).isEmpty();
}
// Run: FAILS - OrderRepository doesn't exist
// 🟢 GREEN: Make it pass (hardcode if needed)
public class OrderRepository {
public List<Order> findAll() {
return Collections.emptyList(); // Simplest implementation
}
}
// Run: PASSES
// 🔵 REFACTOR: Nothing to refactor yet
// Next cycle - 🔴 RED: Add test for non-empty case
@Test
void shouldReturnAllOrders() {
OrderRepository repository = new OrderRepository();
repository.save(new Order("ORD-1"));
repository.save(new Order("ORD-2"));
List<Order> orders = repository.findAll();
assertThat(orders).hasSize(2);
}
// Run: FAILS - save() doesn't exist
// 🟢 GREEN: Add real implementation
public class OrderRepository {
private final List<Order> orders = new ArrayList<>();
public void save(Order order) {
orders.add(order);
}
public List<Order> findAll() {
return new ArrayList<>(orders);
}
}
// Run: PASSES
// 🔵 REFACTOR: Improve test setup
private OrderRepository repository;
@BeforeEach
void setUp() {
repository = new OrderRepository();
}
Example 2: Multiple Cycles Building a Feature
// Cycle 1: 🔴 RED
@Test
void shouldValidatePasswordLength() {
PasswordValidator validator = new PasswordValidator();
assertThat(validator.isValid("123")).isFalse();
assertThat(validator.isValid("12345678")).isTrue();
}
// 🟢 GREEN
public class PasswordValidator {
public boolean isValid(String password) {
return password.length() >= 8;
}
}
// 🔵 REFACTOR
public class PasswordValidator {
private static final int MIN_LENGTH = 8;
public boolean isValid(String password) {
return password.length() >= MIN_LENGTH;
}
}
// Cycle 2: 🔴 RED - Add complexity requirement
@Test
void shouldRequireUppercaseAndLowercase() {
PasswordValidator validator = new PasswordValidator();
assertThat(validator.isValid("alllowercase123")).isFalse();
assertThat(validator.isValid("ALLUPPERCASE123")).isFalse();
assertThat(validator.isValid("MixedCase123")).isTrue();
}
// 🟢 GREEN
public boolean isValid(String password) {
if (password.length() < MIN_LENGTH) return false;
boolean hasUpper = !password.equals(password.toLowerCase());
boolean hasLower = !password.equals(password.toUpperCase());
return hasUpper && hasLower;
}
// 🔵 REFACTOR - Extract methods
public boolean isValid(String password) {
return hasMinLength(password) && hasMixedCase(password);
}
private boolean hasMinLength(String password) {
return password.length() >= MIN_LENGTH;
}
private boolean hasMixedCase(String password) {
boolean hasUpper = !password.equals(password.toLowerCase());
boolean hasLower = !password.equals(password.toUpperCase());
return hasUpper && hasLower;
}
// Cycle 3: 🔴 RED - Add number requirement
@Test
void shouldRequireAtLeastOneNumber() {
PasswordValidator validator = new PasswordValidator();
assertThat(validator.isValid("NoNumbers")).isFalse();
assertThat(validator.isValid("HasNumber1")).isTrue();
}
// 🟢 GREEN
public boolean isValid(String password) {
return hasMinLength(password)
&& hasMixedCase(password)
&& hasNumber(password);
}
private boolean hasNumber(String password) {
return password.matches(".*\\d.*");
}
// 🔵 REFACTOR - Consistent pattern
private boolean hasMixedCase(String password) {
return password.matches(".*[a-z].*") && password.matches(".*[A-Z].*");
}
Example 3: Refactoring to Patterns
// After several Red-Green cycles, we have:
public class OrderService {
public Order createOrder(OrderRequest request) {
Order order = new Order();
order.setCustomerId(request.getCustomerId());
order.setItems(request.getItems());
order.setTotal(calculateTotal(request.getItems()));
order.setStatus("PENDING");
repository.save(order);
emailService.sendConfirmation(order);
analyticsService.trackOrder(order);
return order;
}
}
// 🔵 REFACTOR: Apply Builder pattern
public Order createOrder(OrderRequest request) {
Order order = Order.builder()
.customerId(request.getCustomerId())
.items(request.getItems())
.total(calculateTotal(request.getItems()))
.status(OrderStatus.PENDING)
.build();
repository.save(order);
notifyStakeholders(order);
return order;
}
private void notifyStakeholders(Order order) {
emailService.sendConfirmation(order);
analyticsService.trackOrder(order);
}
Example 4: Test Refactoring
// Tests need refactoring too!
// Before refactoring
@Test
void test1() {
User user = new User();
user.setName("John");
user.setEmail("john@example.com");
user.setAge(25);
service.register(user);
verify(emailService).send("john@example.com", "Welcome", "...");
}
@Test
void test2() {
User user = new User();
user.setName("Jane");
user.setEmail("jane@example.com");
user.setAge(30);
service.register(user);
verify(emailService).send("jane@example.com", "Welcome", "...");
}
// 🔵 REFACTOR: Extract test data builders
@Test
void shouldSendWelcomeEmailToNewUser() {
User user = aUser().withEmail("john@example.com").build();
service.register(user);
verify(emailService).sendWelcomeEmail("john@example.com");
}
@Test
void shouldSendWelcomeEmailWithCorrectContent() {
User user = aUser().withEmail("jane@example.com").build();
service.register(user);
verify(emailService).sendWelcomeEmail("jane@example.com");
}
// Test data builder
private UserBuilder aUser() {
return new UserBuilder()
.withName("Test User")
.withEmail("test@example.com")
.withAge(25);
}
Example 5: Refactoring to SOLID Principles
// After several cycles, we identify SRP violation
// Before refactoring
public class UserService {
public void register(User user) {
validateUser(user);
saveToDatabase(user);
sendWelcomeEmail(user);
logAnalytics(user);
updateCache(user);
}
}
// 🔵 REFACTOR: Single Responsibility
public class UserService {
private final UserValidator validator;
private final UserRepository repository;
private final WelcomeEmailSender emailSender;
private final UserAnalytics analytics;
private final UserCache cache;
public void register(User user) {
validator.validate(user);
repository.save(user);
emailSender.send(user);
analytics.track(user);
cache.update(user);
}
}
// Tests still pass, design improved
Anti-Patterns
❌ Skipping the Red Phase
// WRONG - writing passing test immediately
@Test
void shouldAddNumbers() {
// Test passes immediately - didn't verify it tests anything
}
// ✅ CORRECT - see it fail first
@Test
void shouldAddNumbers() {
Calculator calc = new Calculator();
assertThat(calc.add(2, 3)).isEqualTo(5);
}
// Run: RED (Calculator doesn't exist)
// Then implement
❌ Refactoring During Green Phase
// WRONG - refactoring while making test pass
public int calculate(int a, int b) {
// Making test pass AND refactoring other code
refactorSomeOtherClass();
return a + b;
}
// ✅ CORRECT - separate phases
// GREEN: Just make it pass
public int calculate(int a, int b) {
return a + b;
}
// Then REFACTOR in separate step
❌ Multiple Behaviors Per Cycle
// WRONG - testing multiple behaviors
@Test
void shouldHandleAllCases() {
assertThat(calc.add(1, 1)).isEqualTo(2);
assertThat(calc.subtract(3, 1)).isEqualTo(2);
assertThat(calc.multiply(2, 3)).isEqualTo(6);
}
// ✅ CORRECT - one behavior per test/cycle
@Test
void shouldAddTwoNumbers() {
assertThat(calc.add(1, 1)).isEqualTo(2);
}
Testing Strategies
Cycle Duration Tracking
// Keep cycles short - aim for 1-10 minutes
// If a cycle takes longer, the step is too big
// Example: Break down complex feature
// Instead of: "Implement complete user registration" (60 min)
// Break into:
// 1. Basic user creation (5 min)
// 2. Email validation (5 min)
// 3. Password hashing (8 min)
// 4. Duplicate checking (7 min)
Refactoring Checklist
// During REFACTOR phase, check for:
// ✓ Duplicated code
// ✓ Long methods (>10-15 lines)
// ✓ Poor naming
// ✓ Magic numbers/strings
// ✓ Deep nesting
// ✓ SOLID violations
// After each refactoring:
// ✓ Run all tests - must stay GREEN
// ✓ Commit if tests pass
References
- Test Driven Development: By Example (Kent Beck)
- Refactoring: Improving the Design of Existing Code (Martin Fowler)
- Clean Code (Robert C. Martin)