Factor #7: Resilience - Tests Adapt to Refactoring

Good tests care about what your code does, not how it does it. When you refactor, your tests should still pass. If they don't, you were testing the wrong thing.

Testing Implementation vs Contract

Testing HOW Instead of WHAT
// 😱 BAD: Testing implementation details
class ShoppingCart {
  constructor() {
    this.items = [];
    this.discountRate = 0;
  }
  
  addItem(item) {
    this.items.push(item);
    this._recalculateTotals();
  }
  
  _recalculateTotals() {
    // Private method
    this.subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
    this.total = this.subtotal * (1 - this.discountRate);
  }
}

// BAD TEST - Testing internals
test('cart recalculates on add', () => {
  const cart = new ShoppingCart();
  const spy = jest.spyOn(cart, '_recalculateTotals');
  
  cart.addItem({ price: 10 });
  
  expect(spy).toHaveBeenCalledTimes(1);
  expect(cart.items.length).toBe(1); // Accessing private state
  expect(cart.subtotal).toBe(10);    // Testing internal property
});

// Now refactor to use a different internal structure...
class ShoppingCart {
  constructor() {
    this.itemMap = new Map(); // Changed internal structure
    this.priceCache = null;   // Added caching
  }
  
  addItem(item) {
    this.itemMap.set(item.id, item);
    this.priceCache = null; // Invalidate cache
  }
  
  getTotal() {
    if (!this.priceCache) {
      // Calculate on demand instead
      const items = Array.from(this.itemMap.values());
      this.priceCache = items.reduce((sum, item) => sum + item.price, 0);
    }
    return this.priceCache;
  }
}

// ❌ TEST BREAKS! Even though behavior is identical
Testing the Contract
// ✅ GOOD: Testing behavior/contract
test('cart calculates total correctly', () => {
  const cart = new ShoppingCart();
  
  cart.addItem({ id: 1, name: 'Widget', price: 10 });
  cart.addItem({ id: 2, name: 'Gadget', price: 20 });
  
  expect(cart.getTotal()).toBe(30);
});

test('cart handles discounts', () => {
  const cart = new ShoppingCart();
  
  cart.addItem({ id: 1, price: 100 });
  cart.applyDiscount(0.2); // 20% off
  
  expect(cart.getTotal()).toBe(80);
});

test('cart prevents duplicate items', () => {
  const cart = new ShoppingCart();
  const item = { id: 1, name: 'Widget', price: 10 };
  
  cart.addItem(item);
  cart.addItem(item); // Add same item again
  
  expect(cart.getItemCount()).toBe(1);
  expect(cart.getTotal()).toBe(10);
});

// ✅ These tests still pass after refactoring!
// They test WHAT the cart does, not HOW it does it

Common Brittleness Patterns

Over-Specific Mocking
// 😱 BAD: Testing exact call sequences
test('process order calls services in order', () => {
  const emailMock = jest.fn();
  const inventoryMock = jest.fn();
  const paymentMock = jest.fn();
  
  processOrder(order, { emailMock, inventoryMock, paymentMock });
  
  // Testing specific call order
  expect(paymentMock).toHaveBeenCalledBefore(inventoryMock);
  expect(inventoryMock).toHaveBeenCalledBefore(emailMock);
  
  // Testing exact call arguments
  expect(emailMock).toHaveBeenCalledWith(
    'order@example.com',
    'Order Confirmation',
    expect.stringContaining('Thank you for your order')
  );
});

// ✅ GOOD: Test outcomes, not orchestration
test('process order completes successfully', async () => {
  const result = await processOrder(order);
  
  expect(result.status).toBe('completed');
  expect(result.paymentId).toBeDefined();
  expect(result.trackingNumber).toBeDefined();
  
  // Verify side effects happened (not how)
  const emailSent = await checkEmailQueue(order.customerEmail);
  expect(emailSent).toBe(true);
});
Testing State Transitions
// 😱 BAD: Testing internal state machine
test('order state machine transitions', () => {
  const order = new Order();
  
  expect(order.state).toBe('pending');
  order.validate();
  expect(order.state).toBe('validated');
  order.authorize();
  expect(order.state).toBe('authorized');
  order.capture();
  expect(order.state).toBe('captured');
});

// ✅ GOOD: Test business rules
test('cannot ship unvalidated order', () => {
  const order = new Order({ items: [] });
  
  expect(() => order.ship()).toThrow('Cannot ship invalid order');
});

test('can ship validated paid order', () => {
  const order = new Order({ 
    items: [{ id: 1, price: 10 }],
    payment: { status: 'completed' }
  });
  
  const result = order.ship();
  expect(result.trackingNumber).toBeDefined();
  expect(result.estimatedDelivery).toBeDefined();
});

Building Resilient Tests

1. Test Public APIs Only
If it's not part of the public interface, don't test it
// Only test what consumers can access
class UserService {
  async createUser(email, password) {
    // Public API - test this
  }
  
  _validateEmail(email) {
    // Private - don't test directly
  }
  
  _hashPassword(password) {
    // Private - don't test directly
  }
}
2. Use Integration Tests for Workflows
Test the journey, not the steps
// Instead of testing each step...
test('complete checkout flow', async () => {
  const { customerId } = await createCustomer(userData);
  const { cartId } = await createCart(customerId);
  await addItemsToCart(cartId, items);
  const { orderId } = await checkout(cartId, paymentInfo);
  
  const order = await getOrder(orderId);
  expect(order.status).toBe('completed');
  expect(order.items).toHaveLength(items.length);
});
3. Prefer Black Box Testing
Test as if you can't see the code
// Test like a user would use it
test('calculator performs operations', () => {
  const calc = new Calculator();
  
  calc.enter(5);
  calc.add(3);
  expect(calc.getResult()).toBe(8);
  
  calc.multiply(2);
  expect(calc.getResult()).toBe(16);
  
  calc.clear();
  expect(calc.getResult()).toBe(0);
});
4. Assert on Outcomes
What changed in the world?
// Focus on observable outcomes
test('user registration', async () => {
  await registerUser({ email, password });
  
  // Can the user log in?
  const session = await login(email, password);
  expect(session).toBeDefined();
  
  // Did they get a welcome email?
  const emails = await getEmailsFor(email);
  expect(emails).toContainEqual(
    expect.objectContaining({ subject: 'Welcome!' })
  );
});

The Refactoring Test