Spy — What It Is, Mockito.spy() and Call Verification

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

Spy — a test object that wraps a real instance and records information about each call: which methods were called, with what arguments, and how many times. Unlike a mock, a spy uses the real implementation of the wrapped object — calls go through the actual code, and the spy only records the facts. After the test is executed, the developer checks the spy’s records: “Was the sendAnalytics method called three times?”. Learn more in the Android Testing Guide.

Key Takeaways

  • Spy — an object that records method calls of a real implementation without replacing it
  • Verification — after the test, a spy allows you to check how many times and with what arguments a method was called
  • Mockito.spy() — creates a spy on Android for real Java/Kotlin objects
  • Partial mocking — a spy can be combined with stubs: intercept some methods, let others pass through
  • iOS — OCMock (Objective-C) and manual spies through protocols in Swift

What Is a Spy and How Is It Different from a Mock?

Spy — a wrapper around a real object that intercepts all method calls and records them. The real logic of the object executes: if the method saves data, computes a value, or makes a request — everything happens as usual. Additionally, the spy records metadata: method name, arguments, call count, execution time. The term is part of the Meszaros (2007) classification and is described in detail in Martin Fowler’s article “Mocks Aren’t Stubs”.

Key Difference from a Mock — a mock completely replaces the object with a test stub; all methods do nothing by default. A spy wraps an existing object: all methods work as usual by default, but are also recorded. This distinction is fundamental: a mock isolates the tested code from reality, while a spy preserves reality and allows you to observe it. The choice between them depends on what is being tested.

When a Spy Is the Right Choice

A spy is the right choice — if the tested code modifies the state of a real object, and the test must both verify the result (state) and ensure that calls were made in the correct order. A mock is not suitable because it does not execute the real implementation. A stub is not suitable because it does not record calls. A spy is the only test double that simultaneously preserves real logic and provides information about calls.

Spy vs Mock: When to Use a Spy

Mock — complete isolation. If the test should not depend on the real object’s implementation (for example, a database or network client), use a mock. A mock ensures that no call reaches the real component. This is safe and predictable. The downside: a mock does not execute real logic, so if the tested code relies on a return value, it must be explicitly configured via when/stub.

Spy — real logic + observation. If the tested code interacts with an object whose logic is important for the test, and not just data — use a spy. For example, an AnalyticsTracker that collects events and periodically sends them. The test verifies that events were added to the buffer, and after sending, the buffer was cleared. A mock cannot verify this because it does not execute the tracker’s real logic.

ScenarioSpyMock
Real logic neededYesNo (stub)
Call verificationYes (count, arguments)Yes (count, arguments)
Partial stubbingYes (some methods spy, others stub)No (all methods stubs)
Risk of side effectsHigh (real code)None
SpeedLower (real logic)Higher (stubs)
ReadabilityLower (harder to understand what’s real)Higher (everything is explicit)

Antipattern: spy for everything

Spy for everything — using a spy instead of a mock for all tests is a mistake. A spy executes real code that may have side effects: writing to a file, sending HTTP, changing global state. If the tested module calls a method of a spy object that makes an HTTP request, the test becomes an integration test, not a unit test. Rule: if a spy wraps an object with I/O operations — it’s no longer a unit test. Use mocks to isolate I/O, and spies only for in-memory objects without external effects.

Mockito.spy() and spy in MockK on Android

Mockito.spy() — the classic way to create a spy in Java/Kotlin projects. spy() takes a real object and returns a wrapper. All calls are delegated to the real object by default, and the results are recorded. After the test, you can use verify() to check the call count and arguments. For methods that should return test data, use doReturn/when — this is called “partial mocking”.

kotlin
class AnalyticsReporterTest {

    private val realTracker = AnalyticsTracker()
    private val spyTracker = Mockito.spy(realTracker)

    fun test_event_tracked() {
        val event = AnalyticsEvent("login")
        spyTracker.track(event)

        Mockito.verify(spyTracker).track(event)
        assertEquals(1, spyTracker.getBufferedCount())
    }

    fun test_track_with_exception() {
        Mockito.doThrow(RuntimeException("network"))
            .when(spyTracker).flush()

        spyTracker.track(AnalyticsEvent("login"))
        assertTrue(spyTracker.hasPendingEvents())
    }
}

MockK.spyk() — an alternative for Kotlin projects with better support for coroutines and sealed classes. MockK.spyk() creates a spy, analogous to Mockito.spy(). It supports coVerify for suspend functions and every for partial stubbing. Unlike Mockito, MockK does not support spies for final classes (all classes in Kotlin are final by default) — you need to either open the class (open) or use an interface.

kotlin
class LoginUseCaseTest {

    private val realRepo = UserRepository()
    private val spyRepo = spyk(realRepo)

    private val useCase = LoginUseCase(spyRepo)

    fun test_login_calls_save() = runTest {
        every { spyRepo.getUser(any()) } returns User("test")

        val result = useCase.login("test", "pass")

        coVerify { spyRepo.saveLoginTime(any()) }
        assertTrue(result.isSuccess)
    }
}

Partial Stubbing via Spy

Partial stubbing — a powerful but dangerous technique. You can create a spy of an object and override (stub) only some methods, leaving the rest real. Example: a spy repository where getUser() returns test data, while saveUser() actually saves to an in-memory list. This allows you to combine the benefits of stubs (controlled data) and spies (real logic). The downside: test readability suffers — it’s not obvious which methods are real and which are stubs.

Spy Implementation on iOS with OCMock and Protocols

OCMock for Objective-C — a library that supports creating spy objects via niceMock. OCMock intercepts method calls using the Objective-C runtime and records them. After the test, verify is called. OCMock supports spies for any object (all methods in Objective-C are dynamic), which gives it an advantage over Swift, where spies are only possible through protocols.

objective-c
// Creating a spy for a real object
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];

// Running the test
[spy trackEvent:@"login"];

// Verification
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);

Swift protocol-based spy — Swift does not have Objective-C runtime reflection, so spies are created manually. A test structure implements a protocol and internally calls the real object while simultaneously recording calls. This requires more code, but is fully controllable and type-safe. Manual spies do not require external libraries and do not use runtime — everything is checked at compile time.

swift
protocol AnalyticsProtocol {
    func trackEvent(name: String)
}

final class SpyAnalytics: AnalyticsProtocol {
    private let real: AnalyticsProtocol
    private var events: [String] = []

    init(real: AnalyticsProtocol) {
        self.real = real
    }

    func trackEvent(name: String) {
        events.append(name)
        real.trackEvent(name: name)
    }

    func verifyTracked(name: String) -> Bool {
        return events.contains(name)
    }
}

When to use OCMock vs manual spy — for Objective-C code, use OCMock (less boilerplate). For Swift, manual spies through protocols are preferred. A manual spy gives full control over call recording, requires no reflection, and works with value types (structs). The only downside is that you need to keep the spy class code synchronized with the protocol when adding new methods.

Typical Spy Usage Scenarios

Analytics verification — the most common use case for spies. In production code, analytics calls are scattered throughout the application: login, logout, purchase, error. The test creates a spy wrapper for AnalyticsTracker, executes a scenario (login, view product, add to cart, purchase), and checks that all required events were sent in the correct order. A mock is not suitable because AnalyticsTracker contains buffering and sending logic.

Timers and schedulers — testing code that uses Handler (Android) or Timer (iOS) is difficult due to real time. A spy wrapper for Scheduler records which tasks were scheduled and with what delay. The test creates a spy of the real Handler, performs an action, and checks that Handler.postDelayed(runnable, delay) was called with the correct delay. The real task is not executed — the spy intercepts and records the call.

Logging and debug information — in production, logs may be disabled or written to a file. A spy wrapper for Logger records all messages in an in-memory list that the test checks after execution. This allows verifying that the correct message is written on error without cluttering the console. Manual spies for Logger are especially useful on iOS, where OSLog has no test API.

Call order verification — some scenarios require a strict order of operations: open connection, send data, close connection. Mockito allows checking the order via InOrder.verify(). A spy does the same but preserves real execution. If both the order and the result of each step (the connection actually opened) matter — use a spy, not a mock.

Frequently Asked Questions

Spy vs Mock: What Is the Main Difference?

Spy wraps a real object and executes its logic, additionally recording calls. Mock completely replaces the object with a stub — no real logic is executed. A spy preserves behavior, a mock does not. Choose a spy when the real object’s work matters; choose a mock when you need to isolate the test from an external dependency.

When Is a Spy a Bad Choice?

When a spy wrapper leads to real I/O operations. If a spy wraps an object that writes to a file, sends HTTP, or reads from disk — the test is no longer a unit test. Second case: the test only checks the return value without caring about calls — here a stub is sufficient, and a spy is redundant. Third: the code relies on the internal state of the spy — this is a fragile test.

Does MockK Support Spy?

Yes, via spyk() — the equivalent of Mockito.spy(). MockK.spyk() creates a spy around a real object, supports every for partial stubbing and coVerify/coroutinesVerify for suspend functions. Limitation: does not work with final classes (needs open or interface). For Java classes, MockK also supports spyk() but requires the @MockKJvmInline annotation.

Can You Make a Spy from a Mock?

Technically, no. Mock is a stub that contains no real implementation. A spy, by definition, wraps a real object. In Mockito, you cannot turn a mock into a spy. But you can do the opposite: create a spy and override some methods via doReturn/when (partial mocking). This gives behavior similar to a mock for selected methods of the spy object.

Does a Spy in Swift Require a Protocol?

Yes, it does. In Swift, there is no dynamic proxying like in Java/Kotlin. To create a spy, you need a protocol that both the production class and the spy class implement. A Swift protocol-based spy is a manual implementation that takes a real object, delegates calls to it, and records metadata. Alternative: the Cuckoo library, which generates spy classes via SourceKit.

Summary

  • Spy — a wrapper around a real object that records all method calls without replacing logic
  • Difference from Mock — a spy executes real code, a mock replaces it with a stub
  • Mockito.spy() — for Java/Kotlin, wraps a real object with verify and partial mock support
  • MockK.spyk() — Kotlin equivalent with coroutine support, sealed classes, and coVerify
  • iOS — OCMock for Objective-C, manual protocol-based spies for Swift
  • Primary scenarios — analytics verification, timers, logs, and call order verification
  • Caution — a spy with I/O operations turns a unit test into an integration test

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