
Written by: Senior QA Engineer
Mykhailo RalduhinUpdated: 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:
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.
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?
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:
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:
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.
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.
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.

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