TechLead

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

Continue Learning