Unit Testing Best Practices: Write Tests That Actually Catch Bugs Before Production

Unit Testing Best Practices: Write Tests That Actually Catch Bugs Before Production

Most codebases have tests that do not actually prevent bugs. They test implementation details that change when you refactor. They use mocks so heavily that they never test real behavior. They give false confidence — green tests but broken production. Good testing is about testing behavior, not implementation. This guide covers the patterns that actually catch bugs.

TL;DR: Test behavior, not implementation. Arrange-Act-Assert structure. Mock at the boundary (network, DB, time) not in the middle of your code. Test edge cases: empty, null, boundary values, error paths. Test names should describe what the system does, not which function is called.

What to test vs what not to test

// WRONG: testing implementation details
it('calls the formatDate helper', () => {
  const spy = jest.spyOn(utils, 'formatDate');
  renderComponent({ date: new Date() });
  expect(spy).toHaveBeenCalled(); // This tests HOW not WHAT
  // Passes even if the output is wrong!
  // Breaks when you rename formatDate even if behavior is unchanged
});

// RIGHT: test what the user/caller actually sees
it('displays date in MM/DD/YYYY format', () => {
  const { getByText } = render();
  expect(getByText('04/07/2026')).toBeInTheDocument();
  // Tests BEHAVIOR — passes as long as output is correct
  // Survives any internal refactoring
});

// WRONG: testing third-party library behavior
it('Array.map returns new array', () => { /* ... */ }); // They wrote that test!

// RIGHT: test YOUR code that uses the library
it('filters inactive users and returns sorted names', () => {
  const users = [{ name: 'Bob', active: false }, { name: 'Alice', active: true }];
  expect(getActiveUserNames(users)).toEqual(['Alice']);
});

AAA pattern — Arrange, Act, Assert

// Every test has three clear sections:
describe('UserService.createUser', () => {
  it('sends welcome email after successful account creation', async () => {
    // ARRANGE: set up the world
    const emailService = { sendWelcome: jest.fn().mockResolvedValue(true) };
    const userRepo = { save: jest.fn().mockResolvedValue({ id: '123', name: 'Alice' }) };
    const service = new UserService(userRepo, emailService);

    // ACT: do the thing
    const result = await service.createUser({ name: 'Alice', email: 'alice@example.com' });

    // ASSERT: verify outcomes
    expect(result.id).toBe('123');
    expect(emailService.sendWelcome).toHaveBeenCalledWith(
      expect.objectContaining({ email: 'alice@example.com' })
    );
  });

  it('throws ValidationError when email is invalid', async () => {
    const service = new UserService(mockRepo, mockEmail);
    await expect(service.createUser({ name: 'Alice', email: 'not-an-email' }))
      .rejects.toThrow(ValidationError);
  });
});

Mocking — mock at the boundary

// Mock at external boundaries: network, database, filesystem, time
// Do NOT mock internal functions or modules you wrote

// WRONG: mocking own code deeply
jest.mock('./utils/formatCurrency'); // Now testing with fake implementation
jest.mock('./services/calculateTax'); // Not testing real tax logic!

// RIGHT: mock external dependencies only
jest.mock('./db/client'); // Real boundary — DB calls
jest.mock('./email/smtp'); // Real boundary — email
jest.useFakeTimers(); // Real boundary — time

// Mock strategies:
// Spy: wrap real implementation, track calls
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});

// Stub: replace with fake that returns specific value
const mockRepo = { findById: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }) };

// Fake: working implementation (in-memory DB for testing)
const repo = new InMemoryUserRepository(); // Behaves like real, fast, no I/O

Edge cases — where bugs actually hide

describe('processOrder', () => {
  // Happy path (everyone writes this)
  it('processes valid order', async () => { /* ... */ });

  // Edge cases (where bugs hide)
  it('handles empty items array', async () => {
    await expect(processOrder({ items: [] })).rejects.toThrow('Order must have items');
  });
  it('handles null customer', async () => {
    await expect(processOrder({ customer: null, items: [item] })).rejects.toThrow();
  });
  it('handles item quantity of 0', async () => {
    await expect(processOrder({ items: [{ qty: 0 }] })).rejects.toThrow();
  });
  it('handles negative price', async () => { /* ... */ });
  it('handles order exactly at free shipping threshold', async () => { /* ... */ });
  it('handles concurrent orders for same item (race condition)', async () => { /* ... */ });
  it('rolls back if payment succeeds but inventory update fails', async () => { /* ... */ });
});

Testing best practices cheat sheet

  • ✅ Test behavior, not implementation — tests should survive refactoring
  • ✅ One assertion per test concept — not one assert() per test
  • ✅ Descriptive test names: “does X when Y given Z”
  • ✅ Mock at external boundaries: network, DB, filesystem, time
  • ✅ Test the error paths — that’s where bugs are
  • ✅ Run tests in CI on every commit — fast feedback prevents bugs from shipping
  • ❌ Don’t test private methods — they change. Test the public interface.
  • ❌ Don’t aim for 100% coverage — aim for coverage of critical paths and edge cases

Unit testing discipline directly enables the CI/CD pipeline to work reliably — a green test suite that catches real bugs is what makes automated deployment safe. External reference: Jest best practices documentation.

Recommended Reading

Designing Data-Intensive Applications — Essential for every senior developer. Distributed systems, databases, and production architecture.

The Pragmatic Programmer — Timeless engineering wisdom for writing better code at any level.

Affiliate links. We earn a small commission at no extra cost to you.

Free Weekly Newsletter

🚀 Don’t Miss the Next Cheat Code

Join 1,000+ senior developers getting expert-level JS, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply