Factor #10: Data - Test Data Should Be Explicit

Mystery meat test data is a recipe for confusion. When your test fails six months from now, you should understand the test data instantly, not need archaeology skills.

Test Data Anti-Patterns

The Shared Fixture Nightmare
// 😱 BAD: Shared, mutable test data
// fixtures/users.json
{
  "testUser": {
    "id": 1,
    "email": "test@example.com",
    "balance": 100
  }
}

// test1.js
test('user can make purchase', () => {
  const user = loadFixture('users.json').testUser;
  makePurchase(user, 50);
  expect(user.balance).toBe(50); // Mutates shared data!
});

// test2.js  
test('user can receive refund', () => {
  const user = loadFixture('users.json').testUser;
  // Fails because test1 mutated the balance!
  expect(user.balance).toBe(100); 
  refund(user, 25);
  expect(user.balance).toBe(125);
});
Mystery Meat Data
// 😱 BAD: What do these values mean?
test('calculates shipping cost', () => {
  const order = createOrder(
    3,      // What is 3?
    'US',   // Shipping to or from US?
    true,   // What is true?
    false,  // What is false?
    2       // What is 2?
  );
  
  expect(calculateShipping(order)).toBe(15.99); // Why 15.99?
});
Implicit Dependencies
// 😱 BAD: Hidden data relationships
test('applies bulk discount', () => {
  // Depends on products.json having items with specific IDs
  const cart = new Cart();
  cart.addItem(101, 5); // Assumes product 101 exists
  cart.addItem(102, 3); // Assumes product 102 exists
  
  // Assumes prices in fixture: 101 = $10, 102 = $20
  expect(cart.total()).toBe(110); // How did we get 110?
});

Test Data Patterns

Object Mother Pattern
// ✅ GOOD: Centralized test data creation
class UserMother {
  static simple() {
    return {
      id: generateId(),
      email: 'user@example.com',
      name: 'Test User',
      balance: 0
    };
  }
  
  static withBalance(amount) {
    return {
      ...this.simple(),
      balance: amount
    };
  }
  
  static premium() {
    return {
      ...this.simple(),
      email: 'premium@example.com',
      name: 'Premium User',
      subscription: 'premium',
      balance: 1000
    };
  }
  
  static suspended() {
    return {
      ...this.simple(),
      status: 'suspended',
      suspendedAt: new Date()
    };
  }
}

// Usage
test('premium users get free shipping', () => {
  const user = UserMother.premium();
  const shipping = calculateShipping(user, standardOrder);
  expect(shipping).toBe(0);
});
Test Data Builder Pattern
// ✅ GOOD: Flexible, explicit test data
class OrderBuilder {
  constructor() {
    this.order = {
      id: generateId(),
      items: [],
      shipping: 'standard',
      country: 'US',
      express: false,
      giftWrap: false,
      discount: null
    };
  }
  
  withItems(...items) {
    this.order.items = items;
    return this;
  }
  
  withExpressShipping() {
    this.order.shipping = 'express';
    this.order.express = true;
    return this;
  }
  
  toCountry(country) {
    this.order.country = country;
    return this;
  }
  
  withDiscount(code, percentage) {
    this.order.discount = { code, percentage };
    return this;
  }
  
  build() {
    return { ...this.order };
  }
}

// Usage - Explicit and readable!
test('calculates international express shipping', () => {
  const order = new OrderBuilder()
    .withItems(
      { name: 'Widget', price: 29.99, quantity: 2 },
      { name: 'Gadget', price: 49.99, quantity: 1 }
    )
    .toCountry('UK')
    .withExpressShipping()
    .build();
  
  const shipping = calculateShipping(order);
  expect(shipping).toBe(35.00); // Clear what we're testing
});
Factory Functions
// ✅ GOOD: Simple factories with defaults
function createUser(overrides = {}) {
  return {
    id: uniqueId(),
    email: `test-${Date.now()}@example.com`,
    name: 'Test User',
    createdAt: new Date(),
    verified: true,
    ...overrides  // Explicit overrides last
  };
}

function createProduct(overrides = {}) {
  return {
    id: uniqueId(),
    name: 'Test Product',
    price: 10.00,
    stock: 100,
    category: 'general',
    ...overrides
  };
}

// Usage - minimal and clear
test('out of stock products cannot be purchased', () => {
  const product = createProduct({ stock: 0 });
  const result = purchaseProduct(product, 1);
  
  expect(result.success).toBe(false);
  expect(result.error).toBe('Product out of stock');
});

Best Practices

1. Make Test Data Obvious
The test should tell the whole story
// ❌ BAD: Hidden important details
const user = getTestUser();
expect(canPurchaseAlcohol(user)).toBe(true);

// ✅ GOOD: Relevant data is visible
const user = createUser({ age: 21 });
expect(canPurchaseAlcohol(user)).toBe(true);
2. Avoid Shared Mutable State
Each test gets fresh data
// ❌ BAD: Shared array modified by tests
const sharedItems = [item1, item2, item3];

// ✅ GOOD: Fresh array for each test
function getTestItems() {
  return [
    createItem({ name: 'Item 1' }),
    createItem({ name: 'Item 2' }),
    createItem({ name: 'Item 3' })
  ];
}
3. Use Realistic But Unique Data
Avoid collisions and make debugging easier
// ❌ BAD: Same email in every test
email: 'test@example.com'

// ✅ GOOD: Unique but recognizable
email: `test-${testName}-${Date.now()}@example.com`
email: `user-${uuid()}@test.local`
4. Minimize Test Data
Only include what's relevant to the test
// ❌ BAD: Everything including kitchen sink
const user = {
  id: 1, email: '...', name: '...', 
  address: {...}, preferences: {...},
  history: [...], social: {...}
  // 20 more fields...
};

// ✅ GOOD: Just what we need
const user = {
  email: 'test@example.com',
  subscriptionStatus: 'active'
};

Random vs Deterministic Data