13 KiB
TDD Refactor Phase Prompts
Instructions for Dev and QA agents when refactoring code while maintaining green tests in Test-Driven Development.
Core Refactor Phase Mindset
You are in TDD REFACTOR PHASE. Your mission is to improve code quality while keeping ALL tests green. Every change must preserve existing behavior.
Primary Objectives
- Preserve behavior - External behavior must remain exactly the same
- Improve design - Make code more readable, maintainable, and extensible
- Eliminate technical debt - Remove duplication, improve naming, fix code smells
- Maintain test coverage - All tests must stay green throughout
- Small steps - Make incremental improvements with frequent test runs
Refactoring Safety Rules
The Golden Rule
NEVER proceed with a refactoring step if tests are red. Always revert and try smaller changes.
Safe Refactoring Workflow
refactoring_cycle:
1. identify_smell: 'Find specific code smell to address'
2. plan_change: 'Decide on minimal improvement step'
3. run_tests: 'Ensure all tests are green before starting'
4. make_change: 'Apply single, small refactoring'
5. run_tests: 'Verify tests are still green'
6. commit: 'Save progress if tests pass'
7. repeat: 'Move to next improvement'
abort_conditions:
- tests_turn_red: 'Immediately revert and try smaller step'
- behavior_changes: 'Revert if external interface changes'
- complexity_increases: 'Revert if code becomes harder to understand'
Code Smells and Refactoring Techniques
Duplication Elimination
Before: Repeated validation logic
function createUser(data) {
if (!data.email.includes('@')) {
throw new Error('Invalid email format');
}
return { id: generateId(), ...data };
}
function updateUser(id, data) {
if (!data.email.includes('@')) {
throw new Error('Invalid email format');
}
return { id, ...data };
}
After: Extract validation function
function validateEmail(email) {
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
}
function createUser(data) {
validateEmail(data.email);
return { id: generateId(), ...data };
}
function updateUser(id, data) {
validateEmail(data.email);
return { id, ...data };
}
Long Method Refactoring
Before: Method doing too much
function processUserRegistration(userData) {
// Validation (5 lines)
if (!userData.email.includes('@')) throw new Error('Invalid email');
if (!userData.name || userData.name.trim().length === 0) throw new Error('Name required');
if (userData.age < 18) throw new Error('Must be 18 or older');
// Data transformation (4 lines)
const user = {
id: generateId(),
email: userData.email.toLowerCase(),
name: userData.name.trim(),
age: userData.age,
};
// Business logic (3 lines)
if (userData.age >= 65) {
user.discountEligible = true;
}
return user;
}
After: Extract methods
function validateUserData(userData) {
if (!userData.email.includes('@')) throw new Error('Invalid email');
if (!userData.name || userData.name.trim().length === 0) throw new Error('Name required');
if (userData.age < 18) throw new Error('Must be 18 or older');
}
function normalizeUserData(userData) {
return {
id: generateId(),
email: userData.email.toLowerCase(),
name: userData.name.trim(),
age: userData.age,
};
}
function applyBusinessRules(user) {
if (user.age >= 65) {
user.discountEligible = true;
}
return user;
}
function processUserRegistration(userData) {
validateUserData(userData);
const user = normalizeUserData(userData);
return applyBusinessRules(user);
}
Magic Numbers and Constants
Before: Magic numbers scattered
function calculateShipping(weight) {
if (weight < 5) {
return 4.99;
} else if (weight < 20) {
return 9.99;
} else {
return 19.99;
}
}
After: Named constants
const SHIPPING_RATES = {
LIGHT_WEIGHT_THRESHOLD: 5,
MEDIUM_WEIGHT_THRESHOLD: 20,
LIGHT_SHIPPING_COST: 4.99,
MEDIUM_SHIPPING_COST: 9.99,
HEAVY_SHIPPING_COST: 19.99,
};
function calculateShipping(weight) {
if (weight < SHIPPING_RATES.LIGHT_WEIGHT_THRESHOLD) {
return SHIPPING_RATES.LIGHT_SHIPPING_COST;
} else if (weight < SHIPPING_RATES.MEDIUM_WEIGHT_THRESHOLD) {
return SHIPPING_RATES.MEDIUM_SHIPPING_COST;
} else {
return SHIPPING_RATES.HEAVY_SHIPPING_COST;
}
}
Variable Naming Improvements
Before: Unclear names
function calc(u, p) {
const t = u * p;
const d = t * 0.1;
return t - d;
}
After: Intention-revealing names
function calculateNetPrice(unitPrice, quantity) {
const totalPrice = unitPrice * quantity;
const discount = totalPrice * 0.1;
return totalPrice - discount;
}
Refactoring Strategies by Code Smell
Complex Conditionals
Before: Nested conditions
function determineUserType(user) {
if (user.age >= 18) {
if (user.hasAccount) {
if (user.isPremium) {
return 'premium-member';
} else {
return 'basic-member';
}
} else {
return 'guest-adult';
}
} else {
return 'minor';
}
}
After: Guard clauses and early returns
function determineUserType(user) {
if (user.age < 18) {
return 'minor';
}
if (!user.hasAccount) {
return 'guest-adult';
}
return user.isPremium ? 'premium-member' : 'basic-member';
}
Large Classes (God Object)
Before: Class doing too much
class UserManager {
validateUser(data) {
/* validation logic */
}
createUser(data) {
/* creation logic */
}
sendWelcomeEmail(user) {
/* email logic */
}
logUserActivity(user, action) {
/* logging logic */
}
calculateUserStats(user) {
/* analytics logic */
}
}
After: Single responsibility classes
class UserValidator {
validate(data) {
/* validation logic */
}
}
class UserService {
create(data) {
/* creation logic */
}
}
class EmailService {
sendWelcome(user) {
/* email logic */
}
}
class ActivityLogger {
log(user, action) {
/* logging logic */
}
}
class UserAnalytics {
calculateStats(user) {
/* analytics logic */
}
}
Collaborative Refactoring (Dev + QA)
When to Involve QA Agent
QA Agent should participate when:
qa_involvement_triggers:
test_modification_needed:
- 'Test expectations need updating'
- 'New test cases discovered during refactoring'
- 'Mock strategies need adjustment'
coverage_assessment:
- 'Refactoring exposes untested code paths'
- 'New methods need test coverage'
- 'Test organization needs improvement'
design_validation:
- 'Interface changes affect test structure'
- 'Mocking strategy becomes complex'
- 'Test maintainability concerns'
Dev-QA Collaboration Workflow
collaborative_steps:
1. dev_identifies_refactoring: 'Dev spots code smell'
2. assess_test_impact: 'Both agents review test implications'
3. plan_refactoring: 'Agree on approach and steps'
4. dev_refactors: 'Dev makes incremental changes'
5. qa_validates_tests: 'QA ensures tests remain valid'
6. both_review: 'Joint review of improved code and tests'
Advanced Refactoring Patterns
Extract Interface for Testability
Before: Hard to test due to dependencies
class OrderService {
constructor() {
this.emailSender = new EmailSender();
this.paymentProcessor = new PaymentProcessor();
}
processOrder(order) {
const result = this.paymentProcessor.charge(order.total);
this.emailSender.sendConfirmation(order.customerEmail);
return result;
}
}
After: Dependency injection for testability
class OrderService {
constructor(emailSender, paymentProcessor) {
this.emailSender = emailSender;
this.paymentProcessor = paymentProcessor;
}
processOrder(order) {
const result = this.paymentProcessor.charge(order.total);
this.emailSender.sendConfirmation(order.customerEmail);
return result;
}
}
// Usage in production:
const orderService = new OrderService(new EmailSender(), new PaymentProcessor());
// Usage in tests:
const mockEmail = { sendConfirmation: jest.fn() };
const mockPayment = { charge: jest.fn().mockReturnValue('success') };
const orderService = new OrderService(mockEmail, mockPayment);
Replace Conditional with Polymorphism
Before: Switch statement
function calculateArea(shape) {
switch (shape.type) {
case 'circle':
return Math.PI * shape.radius * shape.radius;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return 0.5 * shape.base * shape.height;
default:
throw new Error('Unknown shape type');
}
}
After: Polymorphic classes
class Circle {
constructor(radius) {
this.radius = radius;
}
calculateArea() {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
calculateArea() {
return this.width * this.height;
}
}
class Triangle {
constructor(base, height) {
this.base = base;
this.height = height;
}
calculateArea() {
return 0.5 * this.base * this.height;
}
}
Refactoring Safety Checks
Before Each Refactoring Step
# 1. Ensure all tests are green
npm test
pytest
go test ./...
# 2. Consider impact
# - Will this change external interfaces?
# - Are there hidden dependencies?
# - Could this affect performance significantly?
# 3. Plan the smallest possible step
# - What's the minimal change that improves code?
# - Can this be broken into smaller steps?
After Each Refactoring Step
# 1. Run tests immediately
npm test
# 2. If tests fail:
git checkout -- . # Revert changes
# Plan smaller refactoring step
# 3. If tests pass:
git add .
git commit -m "REFACTOR: Extract validateEmail function [maintains UC-001, UC-002]"
Refactoring Anti-Patterns
Don't Change Behavior
// Wrong: Changing logic during refactoring
function calculateDiscount(amount) {
// Original: 10% discount
return amount * 0.1;
// Refactored: DON'T change the discount rate
return amount * 0.15; // This changes behavior!
}
// Right: Only improve structure
const DISCOUNT_RATE = 0.1; // Extract constant
function calculateDiscount(amount) {
return amount * DISCOUNT_RATE; // Same behavior
}
Don't Add Features
// Wrong: Adding features during refactoring
function validateUser(userData) {
validateEmail(userData.email); // Existing
validateName(userData.name); // Existing
validateAge(userData.age); // DON'T add new validation
}
// Right: Only improve existing code
function validateUser(userData) {
validateEmail(userData.email);
validateName(userData.name);
// Age validation needs its own failing test first
}
Don't Make Large Changes
// Wrong: Massive refactoring in one step
class UserService {
// Completely rewrite entire class structure
}
// Right: Small, incremental improvements
class UserService {
// Extract one method at a time
// Rename one variable at a time
// Improve one code smell at a time
}
Refactor Phase Checklist
Before considering refactoring complete:
- All tests remain green - No test failures introduced
- Code quality improved - Measurable improvement in readability/maintainability
- No behavior changes - External behavior is identical
- Technical debt reduced - Specific code smells addressed
- Small commits made - Each improvement committed separately
- Documentation updated - Comments and docs reflect changes
- Performance maintained - No significant performance degradation
- Story metadata updated - Refactoring notes and improvements documented
Success Indicators
Refactoring is successful when:
- All tests consistently pass throughout the process
- Code is noticeably easier to read and understand
- Duplication has been eliminated or significantly reduced
- Method/class sizes are more reasonable (functions < 15 lines)
- Variable and function names clearly express intent
- Code complexity has decreased (fewer nested conditions)
- Future changes will be easier due to better structure
Refactoring is complete when:
- No obvious code smells remain in the story scope
- Code quality metrics show improvement
- Tests provide comprehensive safety net
- Ready for next TDD cycle or story completion
Remember: Refactoring is about improving design, not adding features. Keep tests green, make small changes, and focus on making the code better for the next developer!