Unit testing in mobile development: what it is, methods, and frameworks

Author: IT Sectr Published: 2026-04-06 Reading time: 9 min

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 test — verification of a single module (function, method, class) in isolation from external dependencies
  • FIRST principles — Fast, Isolated, Repeatable, Self-validating, Timely — the foundation of quality tests
  • Mocks and stubs — substitutes for external dependencies (DB, API, file system), ensuring test isolation
  • TDD (Test-Driven Development) — test-driven development methodology: red-green-refactor
  • Test pyramid — unit tests make up 70% of the pyramid, providing fast feedback on every commit

What is unit testing?

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.

Why are unit tests needed?

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.

What qualifies as a unit test?

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.

FIRST principles and AAA structure

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.

AAA structure (Arrange-Act-Assert)

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.

kotlin
// 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)
    }
}

Test naming

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.

Mocks, stubs, and fakes: what and when to use

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).

DoubleWhat it verifiesExample
MockMethod call with correct parametersuserRepository.save(user) was called exactly 1 time
StubReturn valuerepository.findById(1) returns User(id=1, name="Test")
FakeLogic via simplified implementationInMemoryMapUserRepository with HashMap instead of DB
SpyPartial mocking of a real objectspy(repo).when(findById).thenReturn(user)

Mockito: mocking example in Java/Kotlin

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.

kotlin
// 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

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.

Benefits of TDD

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.

When is TDD not suitable?

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.

Unit testing in mobile applications

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.

Unit tests on Android (JUnit + Mockito/Robolectric)

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.

Unit tests on iOS (XCTest + Quick/Nimble)

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.

Unit tests on Flutter (flutter_test + Mockito)

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.

dart
// 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);
    });
}

Best practices and common mistakes

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.

  • One assertion per test — one assert or one group of related asserts for one logical check
  • Avoid duplication — use @BeforeEach / setUp for common initialization, parameterized tests for different input data
  • Do not test private methods — test through the public API. If a private method is not covered, its logic is not visible externally
  • Cover edge cases — empty collections, null/undefined, negative numbers, maximum values
  • Do not use Thread.sleep in tests — it makes tests slow and unstable. Use test timeouts and coroutines

What coverage is considered sufficient?

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%.

CI/CD and unit tests

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

How is a unit test different from an integration test?

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%.

Which framework should I choose for unit tests?

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.

What are the F.I.R.S.T. testing principles?

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.

Do I need to write unit tests for ViewModel in Android/iOS?

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.

How to test code with network requests?

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

  • Unit testing — verification of individual modules in isolation from external dependencies with fast feedback
  • AAA structure — Arrange (preparation), Act (action), Assert (verification) — standard test template
  • Mocks and stubs — test doubles for isolation: mocks verify calls, stubs return values
  • TDD — test-driven development (Red-Green-Refactor) reduces defect count by 40-80%
  • FIRST principles — Fast, Isolated, Repeatable, Self-validating, Timely — foundation of quality tests
  • Platform tools — JUnit 5 (Android), XCTest (iOS), flutter_test (Flutter), Jest (React Native)
  • 70-80% coverage — optimal level for critical business logic, getters and setters do not require tests

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.

Discuss the project

Read also