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.
// 😱 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?
// ✅ 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' }); }); });
"Single assertion" doesn't literally mean one `expect()` statement. It means testing one logical concept.
// ✅ 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'); });
When "validates email format" fails, you know the email validation is broken. When "user registration" fails, you know... something about registration is broken?
A focused test typically touches 5-10 lines of production code. An unfocused test might touch hundreds.
❌ test('user service') ❌ test('cart functionality') ✅ test('throws error when item is out of stock') ✅ test('applies discount code to order total')
When you change how emails are sent, only email tests should fail, not every test that happens to create a user.
// 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); }); });
// 😱 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
// ✅ 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
// ✅ 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 });