Factor #5: Focus - Test One Thing at a Time

When a focused test fails, you know exactly what broke. This applies to both individual tests and test suites - keep them small, focused, and modular. Massive test files are as bad as massive test functions.

The Smell of Unfocused Tests

The Monster Test
// 😱 BAD: Testing everything about user registration
test('user registration', () => {
  const user = register({
    email: 'test@example.com',
    password: 'weak',
    age: 15,
    country: 'XX'
  });
  
  // Testing validation
  expect(user.errors).toContain('Password too weak');
  expect(user.errors).toContain('Must be 18 or older');
  expect(user.errors).toContain('Invalid country code');
  
  // Testing success case
  const validUser = register({
    email: 'valid@example.com',
    password: 'Strong123!',
    age: 25,
    country: 'US'
  });
  expect(validUser.id).toBeDefined();
  expect(validUser.email).toBe('valid@example.com');
  
  // Testing email sending
  expect(emailService.sentEmails.length).toBe(1);
  expect(emailService.sentEmails[0].template).toBe('welcome');
  
  // Testing database save
  expect(database.users.length).toBe(1);
  expect(database.users[0].createdAt).toBeDefined();
  
  // Testing analytics
  expect(analytics.events).toContain('user.registered');
});

This test is testing validation, success flow, email sending, database operations, and analytics all at once. When it fails, which part broke?

Focused, Single-Purpose Tests
// ✅ GOOD: Each test has one clear purpose
describe('User Registration', () => {
  test('rejects weak passwords', () => {
    const result = validatePassword('weak');
    expect(result.valid).toBe(false);
    expect(result.error).toBe('Password must contain uppercase, lowercase, and number');
  });
  
  test('requires users to be 18 or older', () => {
    const result = validateAge(15);
    expect(result.valid).toBe(false);
    expect(result.error).toBe('Must be 18 or older');
  });
  
  test('validates country codes', () => {
    const result = validateCountry('XX');
    expect(result.valid).toBe(false);
    expect(result.error).toBe('Invalid country code');
  });
  
  test('creates user with valid data', () => {
    const user = createUser({
      email: 'valid@example.com',
      password: 'Strong123!',
      age: 25,
      country: 'US'
    });
    expect(user.id).toBeDefined();
    expect(user.email).toBe('valid@example.com');
  });
  
  test('sends welcome email on successful registration', () => {
    const emailSpy = jest.spyOn(emailService, 'send');
    registerUser(validUserData);
    expect(emailSpy).toHaveBeenCalledWith({
      to: 'valid@example.com',
      template: 'welcome'
    });
  });
});

The Single Assertion Principle

"Single assertion" doesn't literally mean one `expect()` statement. It means testing one logical concept.

One Concept, Multiple Checks
// ✅ GOOD: Multiple assertions testing one concept
test('formats user name correctly', () => {
  const user = formatUserName('john', 'doe');
  
  // All assertions relate to name formatting
  expect(user.firstName).toBe('John');
  expect(user.lastName).toBe('Doe');
  expect(user.fullName).toBe('John Doe');
  expect(user.initials).toBe('JD');
});

// ✅ GOOD: Testing one behavior thoroughly
test('handles empty shopping cart correctly', () => {
  const cart = new ShoppingCart();
  
  // All assertions verify empty cart behavior
  expect(cart.items).toEqual([]);
  expect(cart.total).toBe(0);
  expect(cart.isEmpty()).toBe(true);
  expect(() => cart.checkout()).toThrow('Cannot checkout empty cart');
});

Benefits of Focused Tests

1. Clear Failure Messages
You know immediately what's broken

When "validates email format" fails, you know the email validation is broken. When "user registration" fails, you know... something about registration is broken?

2. Easier Debugging
Small scope = less code to investigate

A focused test typically touches 5-10 lines of production code. An unfocused test might touch hundreds.

3. Better Test Names
Specific tests lead to descriptive names
❌ test('user service')
❌ test('cart functionality')
✅ test('throws error when item is out of stock')
✅ test('applies discount code to order total')
4. Maintainable Tests
Changes affect fewer tests

When you change how emails are sent, only email tests should fail, not every test that happens to create a user.

Refactoring Monster Tests

Example Refactoring
// BEFORE: One test doing too much
test('order processing', () => {
  const order = new Order(items);
  order.applyDiscount('SAVE10');
  order.setShipping('express');
  order.process();
  
  expect(order.discount).toBe(0.1);
  expect(order.subtotal).toBe(90);
  expect(order.shipping).toBe(15);
  expect(order.total).toBe(105);
  expect(order.status).toBe('processed');
  expect(emailService.sent).toBe(true);
  expect(inventory.updated).toBe(true);
});

// AFTER: Focused tests
describe('Order Processing', () => {
  let order;
  
  beforeEach(() => {
    order = new Order(items);
  });
  
  test('applies percentage discount codes', () => {
    order.applyDiscount('SAVE10');
    expect(order.discount).toBe(0.1);
    expect(order.subtotal).toBe(90);
  });
  
  test('calculates express shipping cost', () => {
    order.setShipping('express');
    expect(order.shipping).toBe(15);
  });
  
  test('calculates total with discount and shipping', () => {
    order.applyDiscount('SAVE10');
    order.setShipping('express');
    expect(order.total).toBe(105);
  });
  
  test('updates order status when processed', () => {
    order.process();
    expect(order.status).toBe('processed');
  });
  
  test('sends confirmation email when processed', () => {
    const spy = jest.spyOn(emailService, 'send');
    order.process();
    expect(spy).toHaveBeenCalledWith(expect.objectContaining({
      type: 'order_confirmation'
    }));
  });
  
  test('updates inventory when processed', () => {
    const spy = jest.spyOn(inventory, 'reserve');
    order.process();
    expect(spy).toHaveBeenCalledWith(items);
  });
});

Modular Test Suites vs Monoliths

The 3000-Line Test File
// 😱 BAD: user.test.js - 3000 lines of everything
describe('User', () => {
  // 50 tests for authentication
  // 30 tests for profile management  
  // 40 tests for permissions
  // 25 tests for notifications
  // 35 tests for data validation
  // ... 2800 more lines
});

// Problems:
// - Can't run subsets easily
// - Merge conflicts constantly
// - No clear organization
// - Slow test discovery
// - Hard to find specific tests
Small, Focused Test Files
// ✅ GOOD: Organized, modular test structure
__tests__/
  user/
    authentication.test.js      // ~200 lines
    profile.test.js            // ~150 lines
    permissions.test.js        // ~250 lines
    notifications.test.js      // ~180 lines
    validation/
      email.test.js           // ~80 lines
      password.test.js        // ~120 lines
      age.test.js            // ~60 lines

// Benefits:
// - Run specific domains: jest user/authentication
// - Parallel test execution by file
// - Clear ownership and organization
// - Easier to maintain and review
// - Can split work across team
Composable Test Utilities
// ✅ GOOD: Shared utilities, not shared state
// __tests__/helpers/user.js
export function createTestUser(overrides = {}) {
  return {
    id: generateId(),
    email: `test-${Date.now()}@example.com`,
    ...overrides
  };
}

// __tests__/user/profile.test.js
import { createTestUser } from '../helpers/user';

test('user can update profile', () => {
  const user = createTestUser({ name: 'Alice' });
  // Test is self-contained but uses shared utilities
});