Factor #11: Assertions - Fail With Helpful Messages

A failing test at 3 AM should tell you what's wrong without requiring a debugging session. "Expected true but got false" is not a helpful message.

Bad Assertion Messages

The Cryptic Failure
// 😱 BAD: What failed? What was it checking?
test('user validation', () => {
  const result = validateUser(userData);
  expect(result).toBe(true);
});

// Failure output:
// ✗ user validation
//   Expected: true
//   Received: false

// 😱 BAD: Which field failed validation?
test('form validation', () => {
  const form = { email: 'bad', age: 'seventeen', name: '' };
  const errors = validateForm(form);
  expect(errors.length).toBe(0);
});

// Failure output:
// ✗ form validation
//   Expected: 0
//   Received: 3
The Magic Number Check
// 😱 BAD: What do these numbers mean?
test('calculates total', () => {
  const result = calculateTotal(order);
  expect(result).toBe(157.45);
});

// Failure output:
// ✗ calculates total
//   Expected: 157.45
//   Received: 162.38

// What changed? Tax? Shipping? Discount? Item prices?

Good Assertion Messages

Descriptive Custom Messages
// ✅ GOOD: Clear context in failure message
test('user validation', () => {
  const result = validateUser(userData);
  expect(result.valid).toBe(true, 
    `User validation failed: ${result.errors.join(', ')}`
  );
});

// Failure output:
// ✗ user validation
//   User validation failed: email format invalid, age must be number

// ✅ GOOD: Show what was being validated
test('form validation', () => {
  const form = { email: 'bad', age: 'seventeen', name: '' };
  const errors = validateForm(form);
  
  expect(errors).toEqual([], 
    `Form validation failed:
     Input: ${JSON.stringify(form, null, 2)}
     Errors: ${JSON.stringify(errors, null, 2)}`
  );
});
Breaking Down Complex Assertions
// ✅ GOOD: Break down the calculation for clarity
test('calculates order total correctly', () => {
  const order = createOrder({
    items: [
      { name: 'Widget', price: 50, quantity: 2 },
      { name: 'Gadget', price: 30, quantity: 1 }
    ],
    discount: 'SAVE10',
    shipping: 'express'
  });
  
  const result = calculateTotal(order);
  
  // Break down the expectation
  const expectedSubtotal = 130;  // (50*2 + 30*1)
  const expectedDiscount = 13;   // 10% off
  const expectedShipping = 15;   // express shipping
  const expectedTax = 14.70;     // 12% tax on (130-13)
  const expectedTotal = expectedSubtotal - expectedDiscount + expectedShipping + expectedTax;
  
  expect(result).toEqual({
    subtotal: expectedSubtotal,
    discount: expectedDiscount,
    shipping: expectedShipping,
    tax: expectedTax,
    total: expectedTotal
  }, `
    Order total calculation mismatch:
    Items: 2x Widget @ $50, 1x Gadget @ $30
    Expected breakdown:
      Subtotal: $${expectedSubtotal}
      Discount (SAVE10): -$${expectedDiscount}
      Shipping (express): +$${expectedShipping}
      Tax (12%): +$${expectedTax}
      Total: $${expectedTotal}
    Actual: $${result.total}
  `);
});

Assertion Patterns

Pattern: Assert with Context
// Provide context about what was being tested
function assertUserCanPurchase(user, product) {
  const canPurchase = checkPurchaseEligibility(user, product);
  
  expect(canPurchase).toBe(true, `
    User ${user.email} cannot purchase ${product.name}:
    - User age: ${user.age} (required: ${product.minAge})
    - User balance: $${user.balance} (price: $${product.price})
    - User verified: ${user.verified}
    - Product available: ${product.inStock}
  `);
}

test('verified adult can purchase age-restricted item', () => {
  const user = createUser({ age: 21, balance: 100, verified: true });
  const product = createProduct({ name: 'Wine', price: 25, minAge: 21 });
  assertUserCanPurchase(user, product);
});
Pattern: Custom Matchers
// Create domain-specific matchers with good messages
expect.extend({
  toBeValidEmail(received) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(received);
    
    return {
      pass,
      message: () => pass
        ? `Expected "${received}" not to be a valid email`
        : `Expected "${received}" to be a valid email address. 
           Common issues: missing @, no domain, spaces`
    };
  },
  
  toBeWithinRange(received, min, max) {
    const pass = received >= min && received <= max;
    
    return {
      pass,
      message: () => pass
        ? `Expected ${received} not to be within range [${min}, ${max}]`
        : `Expected ${received} to be within range [${min}, ${max}]
           Value is ${received < min ? 'too low' : 'too high'} by ${
             Math.abs(received - (received < min ? min : max))
           }`
    };
  }
});

// Usage
test('validates email format', () => {
  expect('not-an-email').toBeValidEmail();
  // Failure: Expected "not-an-email" to be a valid email address.
  //          Common issues: missing @, no domain, spaces
});

test('age within valid range', () => {
  expect(150).toBeWithinRange(18, 120);
  // Failure: Expected 150 to be within range [18, 120]
  //          Value is too high by 30
});
Pattern: Snapshot with Description
// Add context to snapshot tests
test('renders error state correctly', () => {
  const errors = [
    'Email is required',
    'Password too weak',
    'Terms must be accepted'
  ];
  
  const component = render(<Form errors={errors} />);
  
  expect(component).toMatchSnapshot(`
    Form with validation errors:
    - ${errors.length} errors shown
    - Fields: email, password, terms
    - State: initial submission attempt
  `);
});

Best Practices

1. Include Input and Output
Show what went in and what came out
// Show both sides of the equation
expect(result).toBe(expected, `
  Input: ${JSON.stringify(input)}
  Expected: ${expected}
  Actual: ${result}
`);
2. Explain the Why
Don't just show the what, explain why it failed
expect(user.canVote).toBe(true, `
  User cannot vote:
  Age: ${user.age} (must be >= 18)
  Citizenship: ${user.citizenship} (must be valid)
  Registration: ${user.registered} (must be registered)
`);
3. Use Structured Assertions
Compare objects to see all differences at once
// Instead of multiple assertions
expect(user.name).toBe('Alice');
expect(user.age).toBe(25);
expect(user.role).toBe('admin');

// Use object comparison to see all issues
expect(user).toEqual({
  name: 'Alice',
  age: 25,
  role: 'admin'
});
4. Make Async Failures Clear
Timeout? Wrong value? Network error?
await expect(fetchUser(id))
  .rejects
  .toThrow(`Failed to fetch user ${id}: Network timeout after 5s`);

await expect(async () => {
  const user = await fetchUser(id);
  return user.status;
}).rejects.toThrow(`User ${id} is inactive`);