The New Default. Your hub for building smart, fast, and sustainable AI software

See now
Glossary/Data Engineering

Unit Testing

Practice of checking the smallest testable pieces of code, usually a single function or class, in isolation from the rest of the system. Each test gives the unit a known input and confirms it produces the expected result.

What Is Unit Testing?

Every change to a codebase raises the same question: did this break something that already worked? Unit testing answers it for the smallest pieces of the system within seconds, before the change ever leaves the developer's machine.

A unit test calls one piece of code with known inputs and compares the result against an expected value. If a function that calculates an invoice total returns 118.00 for a given order today, a unit test pins that behavior down. When someone later refactors the tax logic and the function starts returning 117.99, the test fails and names the exact function that changed.

What counts as a "unit" is a team decision. Some teams test every function in strict isolation and replace each collaborator with a stand-in. Others treat a small cluster of classes that work together as one unit and only replace dependencies that are slow or unpredictable, such as a database or a third-party API. Both approaches are unit testing. What they share is speed and independence: each test runs fast and does not rely on any other test or on shared state.

Unit tests sit at the base of the test pyramid, a model Mike Cohn popularized in Succeeding with Agile (2009). The pyramid's point is proportion: the cheaper and faster a kind of test is, the more of them a healthy suite should hold.

Why Does Unit Testing Matter?

  • It keeps a codebase cheap to change. Without unit tests, every edit carries unknown risk, and teams respond by editing less. Code that nobody dares to touch hardens around its first design, which is how [technical debt] accumulates. A dense unit suite that checks behavior instead of implementation details turns refactoring into routine work, so the architecture can keep up with the product.

  • It catches defects when fixing them takes minutes. A bug flagged by a failing unit test reaches the person who wrote it while the context is still fresh. The same bug found in staging or production needs someone to reproduce it first and then trace it through code they may not have written.

How Does Unit Testing Work?

Unit testing works as a loop: a runner executes small tests on every change, and the CI pipeline blocks any merge while one of them fails.

  • Arrange, act, assert. Most unit tests follow this named three-step pattern. The test builds the inputs and state the unit needs, calls the unit once, and then compares the result with the expected value. Keeping the steps visibly separate makes a failing test easy to read.

  • Test doubles replace slow or unpredictable dependencies. When a unit depends on a database or an external API, the test swaps in a stand-in called a test double. A stub returns canned answers, while a mock also records how it was called so the test can check the interaction itself.

  • A test runner discovers and executes the suite. Frameworks such as Jest or pytest find test files by naming convention and report each failed assertion with the expected and received values. Most runners can also rerun only the tests affected by a change, which keeps feedback fast as the suite grows.

  • Continuous integration turns the suite into a gate. The [CI/CD] pipeline runs the full unit suite on every push, and a failing test blocks the merge. Without that step, nothing stops a broken test from sitting in the suite for weeks.

  • Coverage tools show what the suite never touches. A coverage report marks which lines and branches executed during the test run. It reveals untested code, but it cannot tell whether the tests that did run check anything meaningful.

  • Test-driven development reverses the order. In TDD, developers follow the red-green-refactor cycle: write a failing test, write just enough code to pass it, then clean up with the test as a safety net. Many teams write tests after the code instead, and both routes produce unit tests.

What Tools Do Teams Use for Unit Testing?

Most stacks pair a test framework with separate tooling for faking dependencies and for judging how much the suite is worth.

Which Frameworks Run Unit Tests?

Jest and Vitest dominate JavaScript and TypeScript projects, with Vitest built to share configuration with the Vite build tool. Python teams mostly use pytest, Java and Kotlin teams use JUnit, and Ruby on Rails teams commonly use RSpec.

Which Libraries Handle Mocking?

Mockito is the standard mocking library for Java. Sinon.js provides stubs and spies for JavaScript, although Jest and Vitest ship their own mocking built in. Python includes unittest.mock in its standard library.

Which Tools Measure Suite Quality?

Istanbul (JavaScript, used by Jest's coverage mode and by nyc), Coverage.py (Python), and JaCoCo (Java) report line and branch coverage. Mutation testing tools such as Stryker and PIT go further: they make small deliberate changes to the code, like flipping > to >=, and check whether any test fails.

What Are the Key Characteristics of Unit Testing?

  • Isolation. A unit test should fail only when the unit under test is wrong. If a test can fail because a network is down or a previous test left data behind, it is testing the environment as well.

  • Speed. Unit tests are fast enough to run on every save. That speed changes developer behavior, because feedback arrives while the change is still in the developer's head.

  • Determinism. The same code gives the same result on every run. A test that passes and fails intermittently, known as a flaky test, trains the team to ignore red builds and undermines the whole suite.

  • Executable documentation. A well-named test such as returns_zero_tax_for_exempt_customers states intended behavior right next to the check that enforces it, which makes the two harder to let drift apart.

What Are the Benefits of Unit Testing?

  • Faster debugging. A failing unit test points to one function and one expectation. Developers start from the cause instead of from a symptom several layers away.

  • Pressure toward better design. Code that is hard to unit test is usually doing too much or reaching into too many dependencies. Writing the test exposes that early, while splitting the code is still cheap.

  • Shorter code reviews. Reviewers can read the tests to see what the author intended. Their attention goes to the logic instead of to re-checking edge cases by hand.

  • Quicker onboarding. New developers can change unfamiliar modules without a senior engineer checking every edit. When a change breaks an assumption they did not know existed, a test tells them immediately.

  • A foundation for frequent releases. A fast, trusted suite replaces much of the manual regression checking before each release. That makes [continuous delivery] practical, since nobody has to re-verify old behavior by hand before every deploy.

What Are the Challenges of Unit Testing?

  • Tests tied to implementation break on harmless changes. Heavy mocking couples a test to how a unit does its job, so a refactor that preserves behavior still turns the suite red. Testing through public interfaces with fewer mocks fixes this, but each test then exercises more code and a failure points less precisely at the cause.

  • Coverage targets invite hollow tests. A mandated coverage percentage rewards tests that execute lines without asserting anything useful. Mutation testing exposes these tests, at the cost of rerunning tests against every code change it introduces, so teams often limit it to nightly runs or changed files.

  • Passing units can still add up to a broken system. A mock encodes the developer's assumption about a dependency, and nothing in the unit suite checks that assumption against the dependency itself. Closing the gap means adding integration or contract tests, which run slower and need more infrastructure to keep working.

  • The suite is code that needs maintenance. Tests need updating when requirements change, and a large suite adds to the cost of every feature. Pruning low-value tests keeps the suite lean, but someone experienced has to judge which tests still earn their place, and that time is rarely budgeted.

  • Legacy code resists isolation. Code written without tests often hides dependencies, like global state or direct database calls, that make isolated tests impossible. Teams working on [legacy system modernization] usually start with characterization tests that record current behavior, a technique from Michael Feathers' Working Effectively with Legacy Code (2004). Those tests make changes safer, but they also lock existing bugs in place until someone decides which behavior is wrong.

What Is the Difference Between Unit Testing and Integration Testing?

Unit testing checks one piece of code in isolation, while integration testing checks that several pieces work correctly together.

Aspect

Unit testing

Integration testing

Scope

One function, method, class, or small cluster of classes

Two or more components, or a component plus a live dependency such as a database

Dependencies

Replaced with test doubles, either all of them or only the slow and unpredictable ones

Some or all are live versions, often in containers or vendor sandboxes

What a failure tells you

Which unit and which expectation broke

That components disagree at a boundary, with the cause still to be located

Blind spot

Wrong assumptions about how dependencies behave

Edge cases inside a unit that the tested path never reaches

When it runs

On every save and every push

On every push or pull request, sometimes only before merge

Share of a healthy suite

The largest layer of the test pyramid

A smaller middle layer

FAq about Unit Testing

Related Terms

Need expert help with Unit Testing?

Monterail builds custom software solutions that leverage the latest technologies. Let's discuss how we can help with your project.

GET IN TOUCH