Unit testing is a software verification method where individual modules or code functions are tested in isolation from the rest of the system. According to Martin Fowler, 2026, unit tests are the foundation of CI/CD and refactoring, providing fast feedback on code correctness. Module testing helps detect errors at early development stages, reducing the cost of fixing them by tens of times.
Key Takeaways
Unit testing is the process of verifying individual units of source code — functions, methods, classes — in isolation from the rest of the program. Each test runs a specific usage scenario of the module and checks that the result matches the expected one. Unit tests are written in the same programming language as the main code and run automatically in the development environment or in a CI/CD pipeline. Unlike integration tests, unit tests do not interact with real databases, file systems, or network services.
The main goal is fast feedback on code correctness after changes. If a developer refactors a method, the unit test suite confirms that behavior has not broken. According to Google Testing Blog (2025), projects with unit test coverage above 60% have 2.5 times fewer production incidents. Additional benefits: code documentation (tests show how to use the API), simplified refactoring (implementation can be changed while preserving behavior), and fast regression diagnostics.
Not every automated test is a unit test. Criteria: a single module (class or function) is tested, external dependencies are replaced with mocks or stubs, the test runs in milliseconds, and does not require starting a server or database. A test that accesses a real database is an integration test. A test that opens a browser is an E2E test. Understanding the boundaries between test types is important for properly allocating efforts in the test pyramid.
Quality unit tests follow the FIRST principles formulated by Robert C. Martin. Each test should be Fast (milliseconds), Isolated (does not depend on other tests), Repeatable (same result on any machine), Self-validating (result is "passed" or "failed", no manual checking), and Timely (written before or simultaneously with the code). Violating any principle reduces the test’s value.
A standard template for writing unit tests. Arrange — prepare data and dependencies: create objects, configure mocks, set input parameters. Act — execute the tested action: call a method or function. Assert — verify the result: compare the actual value with the expected one. Dividing into three blocks makes the test readable and understandable. If the Assert block requires complex logic, the test is likely checking too much at once.
// Example of unit test using AAA pattern in Kotlin with JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — create the test object
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — execute the action
val result = calculator.add(2, 3)
// ASSERT — verify the result
Assertions.assertEquals(5, result)
}
}
The test name should describe what is being tested and what result is expected. Format: [methodName]_[scenario]_[expectedResult]. Example: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. A good test name replaces a comment and immediately indicates which functionality is broken when it fails. Avoid names like test1, checkSomething, or verify — they carry no information and complicate diagnostics.
To isolate the tested module from external dependencies, test doubles are used. Main types: mocks — verify that a specific method was called with the expected parameters; stubs — return predefined values when a method is called; fakes — simplified implementations of real components (e.g., InMemoryUserRepository instead of UserRepository working with a database). The choice depends on what needs to be verified: state (stub) or interaction (mock).
| Double | What it verifies | Example |
|---|---|---|
| Mock | Method call with correct parameters | userRepository.save(user) was called exactly 1 time |
| Stub | Return value | repository.findById(1) returns User(id=1, name="Test") |
| Fake | Logic via simplified implementation | InMemoryMapUserRepository with HashMap instead of DB |
| Spy | Partial mocking of a real object | spy(repo).when(findById).thenReturn(user) |
Mockito is the most popular mocking framework for Java and Kotlin. It allows creating mocks via mock(), configuring return values via when().thenReturn(), and verifying calls via verify(). Modern versions of Mockito (5.x) support static mocks (mockStatic) and simplified syntax through BDDMockito (given-willReturn). An important rule: don’t mock what you don’t own — do not create mocks for value objects and standard libraries.
// Example of unit test with Mockito in Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) is a methodology where a test is written before the code implementation. The "Red-Green-Refactor" cycle: write a test that fails (Red), write minimal code to pass the test (Green), improve the code without changing behavior (Refactor). TDD guarantees that all code is covered by tests (coverage = 100% for implemented functionality) and that the code is testable — if the code is hard to test, the architecture needs improvement.
According to research by IBM (2006-2026, longitudinal study), teams using TDD have 40-80% fewer defects in production compared to teams that write tests after code. TDD also improves architecture: the developer is forced to think about API design before implementation, leading to loose coupling and high cohesion. An additional effect is living documentation: tests serve as a specification of module behavior that is always up-to-date.
TDD is not always optimal. UI components are hard to test in isolation — snapshot tests or visual regression testing (Percy, Chromatic) are more effective. Prototyping and research (spike solutions) do not require tests. Legacy code without tests is hard to cover via TDD — here, characterization tests (tests that capture current behavior before refactoring) are needed first. In these cases, TDD is not completely abandoned but adapted — tests are written for the changed functionality, not for the entire legacy code.
Mobile development has its specifics: business logic is often mixed with UI code (Activity, ViewController, ViewModel), which complicates unit testing. The best practice is thin Views, fat ViewModels: extract all logic from UI components into separate classes (UseCase, Repository, ViewModel) that are easy to test without an emulator. Android and iOS have native unit testing frameworks that run on JVM/Native without launching a device.
Android unit tests run on a local JVM without an emulator, providing execution speed — a typical test takes less than 100ms. JUnit 5 is the main runner. For ViewModel tests, use kotlinx-coroutines-test for coroutine testing and Turbine for StateFlow testing. Robolectric allows testing Android-dependent components (Context, Resources) without an emulator by loading shadow classes. For Compose tests, use Compose UI Test — but these are UI tests, not unit tests.
iOS unit tests are written in Swift with XCTest (built into Xcode). Quick + Nimble are BDD frameworks for more readable tests (describe/context/it). For mocking, use Cuckoo (mock generation) or SwiftyMocky. Swift supports protocols and dependency injection, making it easier to replace dependencies. Key point: iOS unit tests run on the macOS simulator, not on a real device. Tests requiring hardware features (camera, Bluetooth) are integration tests.
Flutter unit tests use the flutter_test package and run on the Dart VM without an emulator. For mocking, use the mockito package with code generation (build_runner). Widget tests (in the same package) test individual widgets but require rendering and run slower — use them only for UI logic verification. Pure Dart logic (models, repositories, blocs) is tested as regular Dart tests without importing flutter_test.
// Example of unit test in Flutter with mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
Effective unit testing requires discipline. The main rule: test behavior, not implementation. The test should not know how the module is implemented internally (what private methods are called, in what order). If a test is tied to implementation, it breaks on every refactoring and loses value. The test verifies the contract: given input X, the output should be Y. An exception is tests for algorithms with critical performance, where the sequence of calls matters.
100% coverage is an unattainable and unnecessary goal. According to Google Testing Blog (2025), the optimal coverage level for unit tests is 70-80% of code lines. 100% coverage is often achieved by testing getters, setters, and constructors, which adds no value. Focus on critical business logic: complex calculations, validation, error handling, edge cases. Use JaCoCo (Java), Coverage.py (Python), Istanbul (JS) for measurement and set a threshold in CI — build failure at coverage below 60%.
Unit tests are the first stage of any CI/CD pipeline. They run on every push to the repository, before build and deployment. The average unit test suite runtime should not exceed 5 minutes — any longer, and tests stop being "fast," and developers stop running them locally. Separate tests into fast (unit) and slow (integration) and run them in different pipeline stages. Use parallel execution and fail-fast for speed.
Frequently Asked Questions
A unit test verifies a single module in isolation, replacing external dependencies with mocks. An integration test verifies interaction between multiple real components (DB, API, file system). Unit tests run in milliseconds, integration tests in seconds. In the test pyramid, unit tests make up 70%.
The choice depends on the platform: JUnit 5 for Java/Kotlin, XCTest for iOS/Swift, pytest for Python, Jest/Vitest for JavaScript/TypeScript, flutter_test for Flutter. For mocking, use Mockito (Java), Cuckoo (iOS), unittest.mock (Python), or vitest.mock (JS). All modern frameworks support parameterized tests, built-in assertions, and parallel execution.
Fast — the test runs in milliseconds. Isolated — does not depend on other tests or external systems. Repeatable — gives the same result on any machine. Self-validating — automatically verifies the result. Timely — written before or synchronously with the code. Violating even one principle reduces testing effectiveness.
Yes, absolutely. ViewModel contains business logic — event handling, data transformation, state management. On Android, use kotlinx-coroutines-test for coroutines and Turbine for StateFlow testing. On iOS, test Combine Publishers or async/await in ViewModel. ViewModel tests are pure unit tests running on JVM/macOS without an emulator.
Network requests in unit tests are not executed — they are replaced with HTTP client mocks. On Android, use MockWebServer (OkHttp) — it runs a local HTTP server, which is preferable to mocks because it simulates real network interaction. MockWebServer provides isolation without losing realism. For iOS — OHHTTPStubs or URLProtocol for intercepting and replacing responses.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also