Contact

What is Unit Test?

Definition

A unit test is an automated test that runs the smallest testable piece of software, usually a single function, method or class, in isolation and checks that a given input produces the expected result. Because unit tests avoid real databases, networks and file systems, they finish in milliseconds, so hundreds of them can run on every change and catch a regression at the moment it is introduced.

Also known as: unit testing, unit tests

Two unit tests checking in isolation that a single function returns the expected results, and the passing test report

Anatomy of a unit test

Most unit tests follow the same three beats: arrange the inputs, act by calling the code, and assert on the result. The example below tests a small function that adds tax to a net price. The syntax is Vitest; Jest, JUnit and pytest look almost the same.

// price.js
export function grossPrice(net, rate = 0.2) {
  if (net < 0) throw new RangeError("Net price cannot be negative");
  return Math.round(net * (1 + rate) * 100) / 100;
}

// price.test.js
import { describe, it, expect } from "vitest";
import { grossPrice } from "./price.js";

describe("grossPrice", () => {
  it("applies the default rate", () => {
    expect(grossPrice(100)).toBe(120);
  });
  it("rounds to whole cents", () => {
    expect(grossPrice(9.99, 0.1)).toBe(10.99);
  });
  it("rejects a negative price", () => {
    expect(() => grossPrice(-1)).toThrow(RangeError);
  });
});

The rounding test earns its place: in floating-point arithmetic 9.99 * 1.1 is not exactly 10.989. If someone deletes the rounding line, that test fails before a one-cent error reaches an invoice. This is what unit tests are best at: pinning down edge cases nobody would think to check by hand, permanently.

Isolation with test doubles

Real code rarely runs alone. It writes to a database, calls a payment provider, reads the clock. In a unit test those collaborators are replaced with test doubles:

  • Stub: returns a canned answer (“the exchange-rate service always returns 1.08”).
  • Mock: records how it was called, so the test can assert “the mailer was called exactly once, with this address”.
  • Fake: a simplified working implementation, such as an in-memory repository.

Isolation keeps tests fast and repeatable, but it is easy to overdo. A test that mocks every internal call verifies how the code is written rather than what it does, and it breaks on a harmless refactor. Brittle suites erode trust until the tests themselves become technical debt. A useful rule: fake only what is slow, non-deterministic or outside your control, such as the network, the clock, randomness and third-party APIs.

Properties of a test worth keeping

  • Fast: the whole suite should run in seconds; slow suites stop being run.
  • Deterministic: the same code gives the same result every time. Pin dates, time zones and random seeds.
  • Independent: no test relies on state left behind by another, and the suite passes in any order.
  • One reason to fail: the name tells you what broke: “rejects a negative price”, not “test1”.
  • Behaviour over internals: exercise the public interface, not private helpers.

Reading code coverage honestly

Coverage reports the share of code executed at least once while the tests ran. Low coverage reliably tells you that some code is never exercised. High coverage proves much less: a test that calls a function and asserts nothing still marks every line as covered. Branch coverage is more informative than line coverage because it shows whether both sides of each if were taken. If you want to know whether the tests would actually catch bugs, mutation testing injects small deliberate faults and measures how many the suite notices. Chasing 100% is expensive and often misleading; full coverage of the critical business rules is a better target.

Where unit tests sit in the pipeline

Unit tests form the wide base of the test pyramid: the most numerous, fastest and cheapest tests you have. They tell you two functions are each correct, not that they are wired together correctly. That is the job of integration tests and of end-to-end testing, which drives the real application through a browser.

Their value comes from running automatically. The usual setup runs the suite on every pull request in the CI/CD pipeline and blocks merging on a red build. When fixing a bug, writing a test that reproduces it first is a habit that pays off: the test fails, the fix makes it pass, and that bug can never quietly return.

Related terms

← Back to the glossary