Mykhailo Ralduhin

Written by: Senior QA Engineer

Mykhailo Ralduhin

Updated: 12.10.2026

18 min read

Unit testing verifies the smallest testable pieces of an application—usually functions, methods, or classes—in isolation from databases, APIs, file systems, and other external dependencies.

It is one of the most widely automated testing levels. In a 2024 survey conducted by the German Testing Board with almost 800 participants, 75% of respondents had automated at least 75% of their unit tests. Only one third reported the same degree of automation at higher test levels, reflecting how much easier unit tests are to run quickly and isolate reliably.

A unit test can be as small as this JavaScript example:

export function add(a, b) {
  return a + b;
}

test('adds two numbers', () => {
  expect(add(2, 3)).toBe(5);
});

The test passes if the function returns 5. If a later change breaks that behavior, the test should fail and show the developer where to investigate.

What is unit testing?

Unit testing is a software testing method in which individual pieces of code are checked independently. A unit may be:

  • a function;
  • a method;
  • a class;
  • a module;
  • a component with a narrowly defined responsibility.

The exact boundary depends on the programming language and architecture. The important condition is isolation: the test should examine one behavior without relying on a real database, network connection, payment gateway, message queue, or other external system.

Unit tests are normally automated and written in the same programming language as the production code. Developers run them locally while implementing a change and again through the CI/CD pipeline.

A good unit test is:

  • fast enough to run frequently;
  • deterministic, producing the same result under the same conditions;
  • independent of other tests;
  • focused on one behavior;
  • easy to understand when it fails;
  • isolated from slow or unpredictable dependencies.

Unit testing is particularly effective for business rules, validation logic, calculations, transformations, state transitions, and error handling.

How to do unit testing step by step

Knowing the syntax of a testing framework is only a small part of unit testing. The harder task is deciding where the unit boundary belongs and which behavior deserves protection.

1. Choose a specific unit of behavior

Select a function, method, or class with a clear responsibility.

Suppose an eCommerce application contains a function that calculates shipping fees. Its expected behavior might be:

  • shipping costs USD 5 for orders below USD 50;
  • shipping is free for orders worth USD 50 or more;
  • members receive free shipping regardless of order value;
  • negative order totals are rejected.

These rules provide a much stronger starting point than a vague goal such as “test the shipping function.”

2. Define the expected outcome

For every scenario, identify:

  • the input;
  • the required initial state;
  • the expected return value or state change;
  • the expected error, if applicable;
  • any interaction that should or should not occur.

Make the expectation specific enough that the test can evaluate it automatically.

3. Isolate external dependencies

A unit test should not need a production database, third-party API, network connection, or shared environment.

Replace these dependencies with:

  • stubs that return predefined values;
  • mocks that record how they were called;
  • fakes that provide lightweight working implementations;
  • dependency injection that allows the test to supply a controlled replacement.

Isolation keeps execution fast and makes failures easier to diagnose.

4. Arrange the test data and dependencies

Create the objects, inputs, and dependency behavior needed for the scenario. Avoid loading a large shared fixture when the test needs only two or three values.

The test should make its relevant setup visible.

5. Execute one behavior

Call the function or method under test. Avoid including multiple unrelated actions in the same unit test.

If one test creates a user, calculates a discount, charges a card, updates inventory, and sends an email, its scope is no longer meaningfully unit-level.

6. Assert the result

Compare the actual result with the expected one. Assertions may check:

  • returned values;
  • thrown errors;
  • state changes;
  • emitted events;
  • calls made to a dependency;
  • calls that must not happen.

A test without a meaningful assertion may execute code without verifying anything useful.

7. Run the test repeatedly

Run the test locally and confirm that it produces the same result every time. Then make a deliberate change to the production code and verify that the test fails for the expected reason.

A test that never fails—even when the protected behavior is broken—does not provide meaningful protection.

8. Add the test to CI

Once reliable, the test should run automatically with other unit tests whenever relevant code changes.

Katalon’s survey of more than 3,800 quality professionals found that 66% considered automated unit testing an effective QA practice. The same study found that 48% struggled with insufficient time to ensure quality and 34% faced difficulties applying test automation. A fast unit suite helps only when running it is part of the normal delivery process rather than an optional task before release.

Unit test example using Arrange, Act, Assert

Arrange, Act, Assert—often abbreviated as AAA—is a simple structure for keeping tests readable.

Consider this shipping function:

export function calculateShipping(orderTotal, isMember) {
  if (orderTotal < 0) {
    throw new Error('Order total cannot be negative');
  }

  if (isMember || orderTotal >= 50) {
    return 0;
  }

  return 5;
}

A Jest test for free shipping could look like this:

import { calculateShipping } from './shipping.js';

test('provides free shipping for orders of $50 or more', () => {
  // Arrange
  const orderTotal = 70;
  const isMember = false;

  // Act
  const shippingFee = calculateShipping(orderTotal, isMember);

  // Assert
  expect(shippingFee).toBe(0);
});

The three parts have distinct responsibilities:

  • Arrange: Prepare the inputs and dependencies.
  • Act: Execute the behavior under test.
  • Assert: Verify the result.

One successful test is not enough for this function. A useful suite should cover the meaningful decision points:

Scenario
Input
Expected result

Standard order

USD 30, non-member

USD 5 shipping

Threshold order

USD 50, non-member

Free shipping

Higher-value order

USD 70, non-member

Free shipping

Member order

USD 20, member

Free shipping

Invalid order

USD −1

Error is thrown

This is a better interpretation of test coverage than writing several tests that exercise the same successful path with slightly different numbers.

Who writes unit tests?

Developers normally write and maintain unit tests because they have direct access to the code and understand its internal design. Unit tests are usually created during implementation, reviewed with the production change, and stored in the same repository.

Current industry data reflects that ownership. Perforce’s 2026 research reports that 53% of developers directly author tests. The report recommends that developers own unit and component testing while QA professionals focus more on system-level risk, analytics, orchestration, and broader quality strategy.

That does not exclude QA engineers. Testers can contribute by:

  • identifying missing boundary and negative scenarios;
  • reviewing acceptance criteria and business rules;
  • advising on testability;
  • pairing with developers on complex cases;
  • checking whether unit coverage reflects product risk;
  • helping teams distinguish unit, integration, and end-to-end coverage.

The healthiest model is collaborative: developers own code-level verification, while QA helps make the overall test model more complete.

Unit testing vs integration testing

Unit testing checks code in isolation. Integration testing checks whether multiple components work correctly together.

Aspect
Unit testing
Integration testing

Scope

One function, method, class, or small module

Multiple components or services

Dependencies

Replaced with mocks, stubs, or fakes

Some or all dependencies are real

Execution speed

Usually milliseconds

Typically slower

Failure diagnosis

Relatively direct

May involve several components

Environment

Local process or lightweight runner

May require databases, containers, APIs, or queues

Main goal

Verify internal logic and edge cases

Verify communication and data flow

Typical owner

Developers

Developers, SDETs, and QA engineers

CI frequency

Every commit or pull request

Build, deployment, scheduled, or pre-release stages

GitLab provides a useful real-world example of a test pyramid. As of February 2025, its documented test distribution consisted of 75.66% unit tests, 19.79% integration tests, 4.31% system tests, and 0.24% black-box end-to-end tests.

Those percentages are not a universal target. They demonstrate the underlying principle: fast, isolated tests should usually form the broadest layer, while slower tests become progressively more selective.

Unit tests cannot prove that the database schema is correct, that two services agree on an API contract, or that a customer can complete an entire workflow. For those risks, teams need integration testing and higher testing levels.

Mocking and dependency isolation

A unit becomes difficult to test when its logic is tightly coupled to external systems.

Suppose a service loads a customer from a repository:

export async function getCustomerName(customerId, repository) {
  const customer = await repository.findById(customerId);
  return customer ? customer.name : 'Guest';
}

A unit test can replace the real repository with a mock:

test('returns the customer name when the customer exists', async () => {
  // Arrange
  const repository = {
    findById: jest.fn().mockResolvedValue({
      id: 42,
      name: 'Alex'
    })
  };

  // Act
  const result = await getCustomerName(42, repository);

  // Assert
  expect(result).toBe('Alex');
  expect(repository.findById).toHaveBeenCalledWith(42);
});

No database is required. The test controls exactly what the repository returns and can focus on the service’s behavior.

Mock, stub, or fake?

Test double
Purpose
Example

Stub

Returns predefined data

A repository always returns one customer

Mock

Verifies interactions

Confirm that an email client was called once

Fake

Provides a lightweight working version

An in-memory repository

Spy

Records calls to a real or partial implementation

Track whether a method was invoked

Mocking should be selective. If every internal method is mocked, tests become tied to implementation details and fail during harmless refactoring.

Mock external boundaries and expensive dependencies—not every collaborator automatically.

Documentation is also frequently incomplete in this area. A 2026 analysis of contribution guidelines in open-source projects found that 71% documented unit testing, but only 9.5% addressed mocking. Teams should therefore document which dependencies to mock, which fakes already exist, and when a test should move to the integration level.

Unit testing in CI/CD and TDD

Unit tests in CI/CD

Unit tests should run early because they provide fast feedback and do not require a deployed environment.

A typical pipeline runs them:

1.
after a developer pushes a commit;
2.
when a pull request is opened or updated;
3.
before building a deployable artifact;
4.
before longer integration or end-to-end suites begin.

DORA recommends keeping CI feedback fast: its continuous integration guidance places the upper limit for automated build and test execution at approximately ten minutes. A focused unit suite should normally consume only part of that budget.

If unit tests become slow, investigate:

  • unnecessary file or network access;
  • databases being started by unit tests;
  • expensive global setup;
  • excessive fixtures;
  • tests running sequentially without a reason;
  • work that belongs in a separate integration suite.

Unit testing and TDD

Test-driven development uses a short cycle:

1.
Red: Write a test for behavior that does not exist yet and watch it fail.
2.
Green: Add the smallest implementation that makes the test pass.
3.
Refactor: Improve the code while keeping the tests green.

TDD is not mandatory for effective unit testing, but adoption has grown. PractiTest’s 2024 State of Testing report found that 23% of respondents used TDD, up from 18% the previous year.

TDD is most helpful when a requirement can be expressed through clear examples. It may be less effective during exploratory prototyping, where the design is changing too quickly for each early assumption to deserve a permanent test.

Unit testing best practices

Test behavior, not implementation

A test should describe what the code promises to do. Avoid asserting private calls, temporary variables, or internal steps unless those interactions are part of the actual contract.

Behavior-focused tests survive refactoring more easily.

Give tests descriptive names

A test name should explain the scenario and expected result:

returns_free_shipping_for_members

is more useful than:

test_shipping_3

Good names reduce the need to open the test file after a CI failure.

Keep tests independent

Do not depend on execution order or state created by another test. Each test should arrange its own data and restore any modified shared state.

Cover boundaries and failures

Happy-path tests are necessary but insufficient. Include:

  • minimum and maximum values;
  • empty and null inputs;
  • invalid formats;
  • permission failures;
  • exceptions from dependencies;
  • duplicate operations;
  • unexpected states.

Keep setup proportional

If a test needs 50 lines of setup to verify one line of behavior, the production design may have too many responsibilities or dependencies.

Builders and factory functions can reduce noise, but excessive abstraction can hide the values that matter to the scenario.

Avoid unnecessary assertions

The traditional rule “one assertion per test” is often interpreted too literally. One behavior may require several related assertions.

A better rule is one reason to fail per test. Checking an order’s status, total, and currency may be reasonable if those fields collectively represent one outcome.

Review test code

Unit tests are part of the codebase. Review them for naming, duplication, false positives, missing edge cases, and unnecessary coupling.

Use coverage as a signal

Coverage helps identify untested code, but a high percentage does not prove that the assertions are useful.

PractiTest’s 2026 report found that 56% of teams were measured on test coverage. Because metrics influence behavior, coverage targets should be paired with mutation testing, risk review, escaped-defect analysis, or checks of critical business paths.

Do not add weak tests solely to increase a dashboard percentage.

Need stronger code-level coverage without slowing delivery?

DeviQA’s unit testing services help teams assess existing coverage, design unit test suites, introduce mocking and testability patterns, and integrate fast checks into CI/CD.

Unit testing tools and frameworks in 2026

Choose a framework that fits the language, build system, IDE, and conventions already used by the team.

Framework
Ecosystem
Best suited for
Notable capabilities

Jest

JavaScript and TypeScript

Node.js and front-end projects

Assertions, mocks, snapshots, coverage

Vitest

JavaScript and TypeScript

Vite-based and modern ESM projects

Fast execution and Jest-compatible APIs

pytest

Python

Applications, libraries, APIs, and data projects

Fixtures, parametrization, plugins

JUnit 5

Java and JVM

Java applications and enterprise systems

Extensions, parameterized tests, tagging

NUnit

.NET

C# and other .NET projects

Parameterized testing, assertions, parallel runs

xUnit.net

.NET

Modern .NET applications

Strong isolation model and data-driven tests

Jest

Jest remains a common choice for JavaScript and TypeScript projects. It provides a test runner, assertion library, mocking, snapshot testing, and coverage integration in one package.

In the State of JavaScript 2024 survey, 7,262 respondents reported using Jest professionally, compared with 3,986 for Vitest. Vitest nevertheless led the survey’s interest, retention, and overall positivity rankings.

Vitest

Vitest is a strong option for applications built with Vite. Its API is familiar to Jest users, while its integration with Vite can simplify configuration and improve performance in modern front-end projects.

Teams migrating from Jest should still verify compatibility for mocking, timers, environment setup, and framework-specific plugins.

pytest

pytest is widely used for Python because its basic test syntax is concise while its fixture and plugin systems support more complex projects.

The Python Developers Survey 2024 collected more than 25,000 valid responses and found that two thirds of Python developers regularly used continuous integration systems. That makes CI compatibility an important practical consideration when choosing and configuring pytest plugins.

JUnit 5

JUnit 5 is the standard modern choice for Java and JVM unit testing. It supports nested tests, parameterized tests, tags, dynamic tests, and a flexible extension model.

It integrates with common Java build tools and CI platforms, including Maven and Gradle.

NUnit

NUnit is a mature framework for .NET languages. It supports fixtures, parameterized tests, setup and teardown hooks, parallel execution, and a broad assertion model.

xUnit.net

xUnit.net is designed around test isolation and is widely used in current .NET development. In JetBrains’ survey of more than 3,800 .NET professionals, 48% of C# developers who wrote unit tests regularly used xUnit.net and 45% used NUnit.

The two frameworks are both viable. Existing conventions, team experience, tooling, and migration cost should carry more weight than small differences in popularity.

Benefits, limitations, and common myths

Unit testing is valuable, but it is neither free nor sufficient by itself.

Claim
Reality

Unit tests always slow development

Writing tests takes time, but fast feedback and safer refactoring may save more time over the product’s life

Every function needs a unit test

Prioritize behavior, risk, branching logic, and stable contracts

High coverage guarantees quality

Coverage proves execution, not the quality of assertions

Unit tests find every defect

They cannot verify real integrations, configuration, usability, or complete workflows

QA should write all tests

Developers normally own unit tests; QA can strengthen scenarios and the overall test strategy

Everything should be mocked

Excessive mocking creates brittle tests tied to implementation

TDD is the only correct approach

TDD is useful but not required for maintaining effective unit tests

Unit tests replace integration testing

Both levels protect different failure modes

The main benefits are:

  • fast feedback during development;
  • safer refactoring;
  • easier diagnosis of logic failures;
  • executable examples of expected behavior;
  • protection for boundary and error conditions;
  • improved pressure toward modular design;
  • reliable checks that can run on every change.

The limitations are equally important:

  • tests require development and maintenance;
  • incorrect tests can provide false confidence;
  • isolated checks cannot validate complete system behavior;
  • excessive mocking may hide integration problems;
  • tests coupled to implementation make refactoring harder;
  • coverage targets can encourage low-value test creation.

Unit testing is most effective as the foundation of a broader testing strategy—not as a replacement for integration, system, exploratory, security, or performance testing.

Frequently asked questions

What is unit testing in simple terms?

Unit testing checks a small piece of code independently to confirm that it produces the expected result for a given input.

For example, a unit test can verify that a discount function returns USD 90 when applying a 10% discount to a USD 100 order.

How is unit testing done?

A developer selects a unit, defines the expected behavior, isolates external dependencies, prepares inputs, executes the code, and asserts the result. The test is then added to the automated CI pipeline.

Is unit testing manual or automated?

Unit testing is normally automated. Manual code inspection and debugging can support the process, but repeatable unit suites are executed with frameworks such as Jest, pytest, JUnit, NUnit, or xUnit.

When should unit tests be written?

They can be written before implementation through TDD, during feature development, or while fixing a defect. A bug fix should usually include a test that fails without the correction and passes with it.

How much unit test coverage is enough?

There is no universal percentage. Critical business logic, calculations, validation, permissions, and error handling deserve stronger coverage than simple configuration or generated code.

Use coverage to locate gaps, then assess whether the tests protect meaningful behavior.

What should not be unit tested?

Avoid unit testing framework internals, trivial generated code, private implementation details, or behavior that can only be verified through real integrations.

Tests that require a database, network, or multiple deployed components normally belong at another level.

What is the difference between a mock and a stub?

A stub returns predefined data. A mock also records interactions so the test can verify how it was called.

Use both selectively. Tests should focus primarily on observable outcomes rather than reproducing every internal interaction.

Can AI write unit tests?

AI tools can draft test cases, suggest edge conditions, generate test data, and explain framework syntax. Developers still need to verify that each assertion represents a real requirement and that the test fails when the protected behavior breaks.

Generated tests should go through the same review process as production code.

Final takeaway

Unit testing gives developers a fast, controlled way to verify business logic before failures spread into integrations and customer-facing workflows. Its value comes from good boundaries, meaningful assertions, reliable isolation, and continuous execution—not from maximizing the number of test files.

Start with critical calculations, validation rules, permissions, and error paths. Keep tests fast enough to run on every change, use mocks only at genuine boundaries, and treat test code as a maintained part of the product.

If your codebase lacks reliable unit coverage or the current suite is slow and difficult to maintain, DeviQA’s unit testing services can help assess the gaps, design an appropriate test architecture, and integrate unit testing into the delivery pipeline.

Mykhailo Ralduhin

About the author

Mykhailo Ralduhin

Senior QA engineer

Mykhailo Ralduhin is a Senior QA Engineer at DeviQA, specializing in building stable, well-structured testing processes for complex software products.