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
});