Unit test

On this page 7

A unit test is an automated test that exercises the smallest testable part of a program, typically a single function, method or class, in isolation from databases, networks and other external systems, and verifies that it produces the expected result. A good unit test runs in milliseconds, gives the same result on every run and pinpoints the exact behavior that broke.

In software testing, unit tests form the broad base of the test pyramid: a codebase usually has many more unit tests than integration tests or end-to-end tests. Frameworks such as JUnit for Java, pytest for Python, Jest for JavaScript, xUnit and NUnit for .NET and the built-in testing package of Go run them automatically, most often on every commit in a continuous integration pipeline.

How a unit test is structured

Most unit tests follow the Arrange, Act, Assert pattern, often abbreviated AAA. The test first prepares inputs and collaborators, then calls the code under test exactly once, and finally checks the outcome with one or more assertions. Behavior-driven teams express the same structure as Given, When, Then.

PhasePurposeExample for a discount function
ArrangeCreate inputs, objects and test doublesA cart with items worth 200.00
ActInvoke the unit under test onceApply a 15% discount code
AssertCompare actual and expected resultsExpect a total of 170.00
The three phases of a unit test

A test name should state the scenario and the expected behavior, for example appliesDiscount_whenCodeIsValid or rejects_negative_quantity. When such a test fails, the name alone tells the developer which rule broke, often before the stack trace is read.

Properties of a good unit test

Experienced practitioners judge unit tests by the FIRST properties. A test that violates them gradually erodes trust in the whole suite, because developers start ignoring failures.

PropertyMeaningTypical violation
FastRuns in millisecondsReal database or network access
IsolatedIndependent of other tests and their orderShared mutable state between tests
RepeatableSame result on every machine and runDependence on the clock or randomness
Self-validatingPasses or fails without human inspectionPrinting output for manual review
TimelyWritten with or before the production codeTests added months after release
The FIRST properties of unit tests

Flaky tests, which pass and fail without any code change, are the most damaging violation. Their usual causes are hidden time dependencies, test-order coupling and asynchronous code that the test does not wait for correctly. A flaky test should be fixed or quarantined promptly, never simply rerun until it passes.

Isolation and test doubles

To test a unit in isolation, its slow or unpredictable dependencies are replaced with test doubles. The generic term covers several kinds, and the mock is only one of them, although everyday speech often calls all of them mocks.

DoubleBehaviorTypical use
DummyPassed but never usedFilling a required parameter
StubReturns fixed, prepared answersSimulating an exchange-rate service
SpyRecords how it was calledChecking that an email was sent once
MockPreprogrammed with expected calls and verifies themVerifying interaction with a payment gateway
FakeWorking but simplified implementationIn-memory repository instead of a database
Kinds of test doubles

Doubles should replace only what is genuinely external or slow. Over-mocking couples tests to implementation details: the test then checks how the code works internally rather than what it produces, and it breaks during every harmless refactoring. A useful rule is to mock roles you own, such as an interface for a repository, and not third-party types directly.

Unit tests compared with other test levels

Unit tests answer one question: does this piece of logic behave correctly on its own? They cannot show that components work together, which is the job of the higher levels in the test pyramid.

AspectUnit testIntegration testEnd-to-end test
ScopeOne function or classSeveral components togetherWhole system through its interface
Typical durationMillisecondsSecondsSeconds to minutes
External systemsReplaced by doublesReal database or serviceReal, deployed environment
Failure diagnosisPreciseModerateOften difficult
Typical share of suiteLargestMediumSmallest
Unit, integration and end-to-end tests compared

Test coverage measures which lines or branches the tests execute, not whether the assertions are meaningful. A suite can reach 100% line coverage while asserting nothing, so coverage is a useful indicator of untested areas but a poor target in itself. In test-driven development (TDD), the unit test is written first, fails, and then guides the smallest implementation that makes it pass.

Common mistakes in unit testing

Weak unit tests often look busy while protecting little. The following mistakes appear in many codebases.

  • Testing private methods directly instead of through the public behavior that uses them.
  • Several unrelated behaviors in one test, so a failure does not show which rule broke.
  • Logic in tests, such as loops and conditionals, which can hide bugs in the test itself.
  • Asserting too little, for example checking only that no exception was thrown.
  • Ignoring edge cases such as empty input, zero, negative values, maximum sizes and null references.

Understanding it simply

A unit test is like checking each part of a bicycle before assembly. The mechanic spins one wheel to see that it runs true, squeezes one brake lever to see that it grips, and does so without riding the whole bicycle down the street.

If a part fails, the mechanic knows exactly which part to fix. Unit tests do the same for software: they check small pieces one at a time, quickly and automatically, every time the code changes.

Applied in practice

A payroll team maintains a function that calculates overtime pay. The rule is that hours beyond 40 in a week are paid at 1.5 times the hourly rate. The team writes unit tests for 40 hours, 45 hours, 0 hours and a negative input, which must be rejected with an error.

For an hourly rate of 20.00 dollars and 45 hours, the test expects 800.00 dollars for the regular hours plus 150.00 dollars for 5 overtime hours, a total of 950.00 dollars. When a developer later refactors the function and accidentally applies the multiplier to all 45 hours, the result becomes 1,350.00 dollars and the test fails within seconds on the developer’s own machine, long before the change reaches a real paycheck. The whole payroll module has about 300 such tests, and they run in under two seconds.