Unit Testing Best Practices
Unit tests verify individual components work correctly in isolation — a single function, method, or class, with its dependencies controlled. They're the fast, focused base of the testing pyramid: when they fail, they point straight at the broken unit, and they run in milliseconds so you can run thousands on every change.
The difference between a test suite that helps and one that fights you comes down to discipline: each test checks one behavior, has a name that explains intent, isolates the unit from the outside world, and tests the cases that actually break — not just the happy path.
TL;DR
- Test one behavior per test (single focus).
- Use descriptive test names that state the expected behavior.
- Isolate the unit with mocks/stubs for external dependencies.
- Follow Arrange-Act-Assert and cover edge cases.
Quick Example
The Arrange-Act-Assert shape, in three clear steps:
Core Concepts
What makes a good unit test (FIRST)
- Fast — milliseconds; you run them constantly.
- Isolated — no external dependencies (network, DB, filesystem).
- Repeatable — same result every run, anywhere.
- Self-validating — pass or fail, no manual interpretation.
- Timely — written with or before the code.
Descriptive naming
One Behavior Per Test
Isolating Dependencies
Replace slow or unpredictable collaborators (HTTP, DB, clock) with test doubles so the test exercises your unit:
See Mocking & Stubbing.
Test Edge Cases
The happy path rarely breaks — the boundaries do:
Common Patterns
Fixtures
Share setup without copy-paste:
Parameterized tests
One test, many cases:
Best Practices
- One behavior per test — failures should localize the cause.
- Name tests as specs — describe the expected behavior, not "test1".
- Arrange-Act-Assert — keep the three phases visually distinct.
- Isolate the unit — mock external dependencies, but don't over-mock.
- Cover edge cases — empty, zero, negative, boundary, error paths.
- Keep tests independent — no shared mutable state or ordering assumptions.
Common Mistakes
Testing implementation instead of behavior
Over-mocking
FAQ
How many assertions should a test have?
Aim for one behavior per test — which often means one logical assertion, though a few assertions checking facets of the same outcome are fine. The goal isn't a literal single assert; it's that a failing test points to exactly one broken behavior. Avoid bundling unrelated checks into one test, since the first failure hides the rest.
What's the difference between a mock and a stub?
A stub provides canned answers to calls (it feeds data in); a mock also verifies that the right calls happened (it checks interactions). Use stubs to control inputs and mocks when the interaction itself is the behavior under test. Over-using interaction mocks couples tests to implementation — see Mocking & Stubbing.
Should I test private methods directly?
Usually no — test private behavior through the public interface. If a private method is complex enough to want its own tests, that's often a signal it should be extracted into its own unit (a separate function/class) with a public API worth testing. Testing privates directly couples tests to implementation and breaks on refactors.
How do I unit-test code that uses the current time, randomness, or the network?
Inject those dependencies rather than reaching for them globally. Pass in a clock, a random source, or an HTTP client so tests can supply deterministic fakes. This is the core of testable design — and the same dependency injection that TDD pushes you toward. See TDD.
Related Topics
- TDD — Test-driven development
- Mocking & Stubbing — Isolating dependencies
- Integration Testing — Testing units together
- Test Coverage — Measuring what's tested
- Jest · Pytest · Vitest — Frameworks