Testing Strategies & Anti-Patterns
Master the test pyramid, testing trophy, AAA pattern, test naming, isolation strategies, and learn when NOT to write tests
Test Pyramid vs Testing Trophy
The Test Pyramid (by Mike Cohn) suggests many unit tests, fewer integration tests, and few E2E tests. The Testing Trophy (by Kent C. Dodds) emphasizes integration tests as the sweet spot for confidence-per-effort. Both models are useful -- the right balance depends on your application.
Test Pyramid
E2E (few)
Integration (some)
Unit Tests (many)
Traditional server-side approach
Testing Trophy
E2E (few)
Integration (many)
Unit (some)
Static (TS/ESLint)
Modern frontend approach
The AAA Pattern & Given-When-Then
// AAA Pattern: Arrange, Act, Assert
test('applies percentage discount to order total', () => {
// Arrange - set up test data and dependencies
const order = createOrder([
{ name: 'Widget', price: 25.00, quantity: 2 },
{ name: 'Gadget', price: 50.00, quantity: 1 },
]);
const discount = { type: 'percentage', value: 10 };
// Act - perform the action being tested
const result = applyDiscount(order, discount);
// Assert - verify the expected outcome
expect(result.total).toBe(90.00); // (50 + 50) * 0.9
expect(result.savings).toBe(10.00);
});
// Given-When-Then (BDD style)
describe('Shopping Cart', () => {
describe('given a cart with items', () => {
const cart = new Cart();
beforeEach(() => {
cart.add({ id: '1', name: 'Book', price: 15 });
cart.add({ id: '2', name: 'Pen', price: 3 });
});
describe('when removing an item', () => {
beforeEach(() => cart.remove('1'));
it('then should not contain the removed item', () => {
expect(cart.items).not.toContainEqual(
expect.objectContaining({ id: '1' })
);
});
it('then should update the total', () => {
expect(cart.total).toBe(3);
});
});
});
});
Test Naming Conventions
// Good: Describes behavior from user/caller perspective
test('returns null when user is not found', () => {});
test('sends welcome email after registration', () => {});
test('disables submit button while form is submitting', () => {});
test('shows error message when API returns 500', () => {});
// Bad: Describes implementation details
test('calls setUser with null', () => {});
test('triggers useEffect', () => {});
test('sets isLoading to true', () => {});
test('renders div with class error', () => {});
// Pattern: [unit] + [scenario] + [expected result]
describe('calculateShipping', () => {
test('returns free shipping for orders over $50', () => {});
test('returns $5.99 for standard domestic shipping', () => {});
test('returns $15.99 for international shipping', () => {});
test('throws error for invalid address', () => {});
});
// Use test.each for parameterized tests
test.each([
{ input: 'hello', expected: 'HELLO' },
{ input: 'World', expected: 'WORLD' },
{ input: '', expected: '' },
])('toUpperCase("$input") returns "$expected"', ({ input, expected }) => {
expect(toUpperCase(input)).toBe(expected);
});
Test Isolation & Avoiding Brittle Tests
// BAD: Tests depend on each other
let sharedUser;
test('creates a user', () => {
sharedUser = createUser('Alice');
expect(sharedUser.id).toBeDefined();
});
test('updates the user', () => {
// Fails if previous test fails!
updateUser(sharedUser.id, { name: 'Bob' });
});
// GOOD: Each test is independent
test('creates a user', () => {
const user = createUser('Alice');
expect(user.id).toBeDefined();
});
test('updates a user', () => {
const user = createUser('Alice'); // Own setup
const updated = updateUser(user.id, { name: 'Bob' });
expect(updated.name).toBe('Bob');
});
// BAD: Brittle - testing implementation details
test('renders correctly', () => {
const { container } = render(<Button />);
expect(container.querySelector('.btn-primary-lg')).toBeTruthy();
});
// GOOD: Testing behavior
test('renders clickable button', () => {
render(<Button>Submit</Button>);
expect(screen.getByRole('button', { name: 'Submit' })).toBeEnabled();
});
// BAD: Snapshot everything
test('matches snapshot', () => {
const { container } = render(<EntireApp />);
expect(container).toMatchSnapshot(); // Breaks on any change
});
// GOOD: Targeted assertions
test('displays user profile', () => {
render(<UserProfile user={mockUser} />);
expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
When NOT to Test
Worth Testing:
- Business logic and calculations
- Data transformations and validation
- User-facing workflows
- Error handling and edge cases
- Security-critical code paths
- Complex conditional logic
Usually NOT Worth Testing:
- Third-party library internals
- Simple getters/setters with no logic
- Framework boilerplate and config
- One-off scripts and prototypes
- Pure CSS/styling (use visual tests instead)
- Constants and type definitions
Key Takeaways
- Choose your test distribution (pyramid vs trophy) based on your architecture
- Use AAA or Given-When-Then consistently for readable, maintainable tests
- Name tests by behavior and expected outcome, not implementation details
- Every test should be independent -- no shared mutable state between tests
- Test the things that matter most; not everything needs a test