Fast feedback testing

Writing Effective Unit Tests

Test observable behavior with focused examples, controlled dependencies, clear failures, and deliberate boundary coverage.

How this page is maintained

Written for learners, checked against the sources below, and reviewed every year. Last reviewed July 27, 2026.

Short answer

An effective unit test exercises one meaningful behavior through a small public boundary, controls slow or nondeterministic dependencies, and makes the expected outcome obvious. It should fail when behavior breaks, not when harmless implementation details change. Unit tests are one layer: integration and end-to-end tests remain necessary for wiring and real user paths.

Who this is for: Developers writing automated tests for functions, modules, components, and domain rules that change frequently.

  • Choose examples from rules, boundaries, and past failures rather than chasing a coverage percentage alone.
  • Assert observable outputs and effects while avoiding mocks of the unit's own internal steps.
  • Keep tests deterministic, isolated, readable, and fast enough to run during ordinary development.

Define the behavior under test

Begin with a rule in plain language: an expired coupon is rejected, a subtotal excludes cancelled lines, or a parser reports an unknown status. Select the smallest public function or module boundary that expresses the rule. A unit can include several collaborating functions when splitting them would expose implementation rather than useful behavior.

Name the test around condition and outcome. Arrange only the data needed, perform one meaningful action, and assert the visible result. This structure is guidance, not ceremony. A test with many unrelated assertions likely covers several behaviors and will produce an ambiguous failure when one changes.

Select cases with reasoning

Cover a representative normal case, important boundaries, invalid inputs, and business exceptions. For numeric ranges, test just below, at, and just above a threshold. For state machines, test allowed and forbidden transitions. Add a regression case when a real defect reveals a missing distinction, then fix the implementation.

Coverage can identify code never executed, but it does not show whether assertions are meaningful. One test can execute every line while checking the wrong outcome. Use coverage as a question generator. Risk, complexity, and consequences should drive depth. Generated combinations can help pure algorithms, while curated cases communicate domain intent better for many rules.

Control dependencies at the boundary

Time, randomness, network calls, files, and process-wide configuration can make a unit test slow or unpredictable. Pass a clock, identifier generator, repository, or sender through a clear dependency boundary when the code needs control. Use a simple fake or stub that behaves like the dependency contract required by the test.

Avoid mocking every internal function call. Tests that assert call order inside the unit prevent refactoring while offering little product confidence. Mock external effects when necessary, and assert the meaningful request sent to that boundary. Separate contract or integration tests should verify that a fake still represents the real dependency closely enough.

Make failures useful and stable

A test should produce the same result regardless of run order, timezone, machine speed, or unrelated tests. Reset shared state, use fixed time deliberately, await asynchronous work, and avoid arbitrary sleeps. If concurrency is the behavior, expose a controllable synchronization point and assert the final invariant.

Prefer focused equality or structured matchers that show the difference. Large snapshots can hide meaningful changes among formatting noise and may be approved without review. Keep test data close enough to understand, but use builders when repeated setup obscures the rule. Run the suite in continuous integration and keep flaky tests as defects, not accepted background noise.

Test a shipping discount rule

Orders receive free standard shipping at a subtotal threshold, but gift cards and cancelled lines do not count.

  1. Express the unit as a function from active purchasable lines and delivery method to a shipping charge decision.
  2. Test one subtotal below the threshold, exactly at it, and above it with standard delivery.
  3. Add cases where gift-card value or a cancelled line would incorrectly push the raw total over the threshold.
  4. Test that express delivery remains charged if that is the written rule, even above the standard threshold.
  5. Refactor the calculation and confirm tests keep passing without asserting private helper calls.
Result: The tests document the threshold and exclusions, catch the likely boundary defects, and permit internal calculation changes.

Unit test case card

Use this card to justify each test instead of adding examples mechanically.

  • Rule: observable behavior, public boundary, business consequence, and source of expected result.
  • Case: normal input, boundary, invalid input, exception, or regression represented.
  • Dependencies: real pure collaborator, fake, stub, fixed clock, deterministic identifier, and cleanup.
  • Assertion: returned value, state change, emitted command, error, and details intentionally ignored.
  • Quality: independent order, useful failure, runtime cost, refactoring tolerance, and missing higher-level test.

Common mistakes

  • Asserting private helper calls so harmless refactoring breaks tests without changing behavior.
  • Using arbitrary time delays to wait for asynchronous code and accepting intermittent failures.
  • Treating high line coverage as proof that important rules and boundaries are correct.

Try one

Choose unit test cases for a password-reset token that expires after thirty minutes and can be used once. Explain what unit tests cannot establish.

Good cases use a fixed clock to test just before, exactly at, and after the documented boundary, plus already-used and wrong-token cases. They assert one successful transition and deliberate rejection reasons without real waiting. Unit tests cannot prove database uniqueness, transaction behavior, email links, or the complete browser flow, so those require integration and end-to-end coverage.

Sources

Learn this with a tutor

Tell LearnLive what you already know and what you need to do with effective unit tests.

Build this course