BMAD-METHOD/bmad-core/prompts/tdd-refactor.md

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

  1. Preserve behavior - External behavior must remain exactly the same
  2. Improve design - Make code more readable, maintainable, and extensible
  3. Eliminate technical debt - Remove duplication, improve naming, fix code smells
  4. Maintain test coverage - All tests must stay green throughout
  5. 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:

  1. All tests consistently pass throughout the process
  2. Code is noticeably easier to read and understand
  3. Duplication has been eliminated or significantly reduced
  4. Method/class sizes are more reasonable (functions < 15 lines)
  5. Variable and function names clearly express intent
  6. Code complexity has decreased (fewer nested conditions)
  7. 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!