Spy — 什么是 Spy、Mockito.spy() 和调用验证

作者: IT Sectr 发布日期: 2026-04-10 阅读时间: 9 分钟

Spy(间谍)——一种测试对象,它包装一个真实实例并记录关于每次调用的信息:哪些方法被调用,使用什么参数,调用次数。与 mock 不同,spy 使用被包装对象的真实实现——调用通过真实代码传递,而 spy 仅记录事实。测试执行后,开发者检查间谍的记录:「sendAnalytics 方法被调用了三次?」更多信息请参阅 Android 测试指南

要点

  • Spy——记录真实实现的方法调用而不替换它的对象
  • 验证——测试后,spy 允许检查方法被调用了多少次以及使用什么参数
  • Mockito.spy()——在 Android 上为真实的 Java/Kotlin 对象创建间谍
  • Partial mocking——spy 可以与 stub 组合:拦截一些方法,放行其他方法
  • iOS——OCMock(Objective-C)以及 Swift 中通过协议实现的手动间谍

什么是 Spy,它与 Mock 有什么区别?

Spy——是围绕真实对象的包装器,它拦截所有方法调用并记录它们。对象的真实逻辑被执行:如果方法保存数据、计算值或发出请求——一切照常发生。此外,spy 记录元数据:方法名称、参数、调用次数、执行时间。该术语属于 Meszaros(2007)分类法,并在 Martin Fowler 的文章「Mocks Aren't Stubs」中详细描述。

与 Mock 的关键区别——mock 完全用测试存根替换对象,所有方法默认什么都不做。Spy 包装现有对象:所有方法默认照常工作,但同时被记录。这个区别是根本性的:mock 将测试代码与现实隔离,spy 保留现实并允许观察它。它们之间的选择取决于测试的内容。

何时 Spy 是正确的选择

Spy——正确的选择——如果被测试的代码修改了真实对象的状态,并且测试既要检查结果(状态),又要确保调用按正确顺序进行。Mock 不适合,因为它不执行真实实现。Stub 不适合,因为它不记录调用。Spy 是唯一同时保留真实逻辑并提供调用信息的测试替身。

Spy vs Mock:何时使用间谍

Mock——完全隔离。如果测试不应依赖于真实对象的实现(例如数据库或网络客户端),请使用 mock。Mock 保证没有调用会到达真实组件。这很安全且可预测。缺点:mock 不执行真实逻辑,因此如果测试代码依赖于返回值——必须通过 when/stub 显式配置。

Spy——真实逻辑 + 观察。如果被测试的代码与一个逻辑(而非仅仅是数据)对于测试很重要的对象交互——请使用 spy。例如,收集事件并定期发送它们的 AnalyticsTracker。测试检查事件是否已添加到缓冲区,以及发送后缓冲区是否已清除。Mock 无法检查这一点,因为它不执行 tracker 的真实逻辑。

场景SpyMock
需要真实逻辑否(存根)
调用验证是(次数、参数)是(次数、参数)
部分存根是(部分方法——spy,其余——stub)否(所有方法——存根)
副作用风险高(真实代码)
速度较低(真实逻辑)较高(存根)
可读性较低(更难理解什么是真实的)较高(一切显式)

反模式:万事用 spy

万事用 spy——在所有测试中使用 spy 代替 mock 是个错误。Spy 执行真实代码,可能有副作用:写入文件、发送 HTTP、更改全局状态。如果测试模块调用进行 HTTP 请求的 spy 对象的方法,测试变成集成测试,而不是单元测试。规则:如果 spy 包装带有 I/O 操作的对象——这不再是单元测试。使用 mock 进行 I/O 隔离,spy——仅用于没有外部影响的内存对象。

Android 上的 Mockito.spy() 和 MockK 中的 spy

Mockito.spy()——在 Java/Kotlin 项目中创建间谍的经典方法。spy() 接收一个真实对象并返回一个包装器。所有调用默认委托给真实对象,结果被记录。测试执行后,可以通过 verify() 检查调用次数和参数。对于应该返回测试数据的方法,使用 doReturn/when——这被称为「部分存根」(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()——适用于 Kotlin 项目的替代方案,更好地支持协程和密封类。MockK.spyk() 创建间谍,类似于 Mockito.spy()。支持用于挂起函数的 coVerify 和用于部分存根的 every。与 Mockito 不同,MockK 不支持 final 类的 spy(Kotlin 中所有类默认都是 final 的)——需要打开类(open)或使用接口。

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

通过 spy 进行部分存根

部分存根——一种强大但危险的技术。您可以创建一个对象的 spy 并仅覆盖(stub)部分方法,将其他方法保留为真实的。例如:一个 spy 仓库,其 getUser() 返回测试数据,而 saveUser() 实际保存到内存列表。这允许结合存根(可控数据)和 spy(真实逻辑)的优点。缺点:测试的阅读复杂性——不清楚哪些方法是真实的,哪些是存根。

iOS 上使用 OCMock 和协议的 Spy 实现

OCMock for Objective-C——支持通过 niceMock 创建 spy 对象的库。OCMock 使用 Objective-C 运行时拦截方法调用并记录它们。测试执行后调用 verify。OCMock 支持任意对象的 spy(在 Objective-C 中所有方法都是动态的),这比 Swift 具有优势,在 Swift 中 spy 只能通过协议实现。

objective-c
// 为真实对象创建 spy
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];

// 执行测试
[spy trackEvent:@"login"];

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

Swift protocol-based spy——在 Swift 中没有 Objective-C 运行时反射,因此 spy 是手动创建的。测试结构实现协议并在内部调用真实对象,同时记录调用。这需要更多代码,但完全可控且类型安全。手动间谍不需要外部库,也不使用运行时——一切在编译阶段检查。

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

何时使用 OCMock 与手动 spy——对于 Objective-C 代码请使用 OCMock(更少样板代码)。对于 Swift——推荐通过协议使用手动间谍。手动 spy 提供对调用记录的完全控制,不需要反射,并且适用于值类型(struct)。唯一缺点:在添加新方法时需要保持 spy 类代码与协议同步。

Spy 的典型应用场景

检查分析——spy 最常见的应用场景。在生产代码中,分析调用遍布整个应用程序:登录、注销、购买、错误。测试为 AnalyticsTracker 创建 spy 包装器,执行场景(登录、查看产品、添加到购物车、购买)并检查所有必要的事件是否按正确顺序发送。Mock 不适合,因为 AnalyticsTracker 包含缓冲和发送逻辑。

定时器和调度器——测试使用 Handler(Android)或 Timer(iOS)的代码由于实时性而困难。Scheduler 的 spy 包装器记录哪些任务被调度以及以什么延迟。测试创建真实 Handler 的 spy,执行操作并检查 Handler.postDelayed(runnable, delay) 是否以正确的延迟被调用。真实任务在此期间不执行——spy 拦截并记录调用。

日志记录和调试信息——在生产中,日志可能被关闭或写入文件。Logger 的 spy 包装器将所有消息记录到内存列表中,测试在执行后检查该列表。这允许检查错误时是否写入正确的消息,而不污染控制台。Logger 的手动间谍在 iOS 上尤其有用,因为 OSLog 没有测试 API。

检查调用顺序——某些场景需要严格的操作顺序:打开连接、发送数据、关闭连接。Mockito 允许通过 InOrder.verify() 检查顺序。Spy 做同样的事情但保留真实执行。如果不仅顺序重要,每个步骤的结果也很重要(连接确实已打开)——请使用 spy,而不是 mock。

常见问题

Spy vs Mock:主要区别是什么?

Spy 包装真实对象并执行其逻辑,同时额外记录调用。Mock 完全用存根替换对象——不执行任何真实逻辑。Spy 保留行为,mock 不保留。当对象的真实工作重要时选择 spy;当需要将测试与外部依赖隔离时选择 mock。

何时 Spy 是不好的选择?

当 spy 包装器导致真实的 I/O 操作时。如果 spy 包装写入文件、发送 HTTP 或从磁盘读取的对象——测试不再是单元测试。第二种情况:测试只检查返回值而不关心调用——这里 stub 就足够了,spy 是多余的。第三种:代码依赖于 spy 的内部状态——这是一个脆弱的测试。

MockK 是否支持 spy?

是的,通过 spyk()——类似于 Mockito.spy()。MockK.spyk() 在真实对象周围创建间谍,支持用于部分存根的 every 和用于挂起函数的 coVerify/coroutinesVerify。限制:不适用于 final 类(需要 open 或接口)。对于 Java 类,MockK 也支持 spyk(),但需要 @MockKJvmInline 注解。

可以从 Mock 创建 Spy 吗?

技术上——不能。Mock 是不包含真实实现的存根。Spy 根据定义包装真实对象。在 Mockito 中无法将 mock 转换为 spy。但反过来可以:创建一个 spy 并通过 doReturn/when(partial mocking)覆盖部分方法。这对 spy 对象的选定方法提供了类似于 mock 的行为。

Swift 中的 Spy 必须通过协议吗?

必须。在 Swift 中没有像 Java/Kotlin 那样的动态代理。要创建 spy,需要一个由生产类和 spy 类都实现的协议。Swift-protocol-based spy 是一种手动实现,它接收真实对象,将调用委托给它,并记录元数据。替代方案:Cuckoo 库,它通过 SourceKit 生成 spy 类。

总结

  • Spy——围绕真实对象的包装器,记录所有方法调用而不替换逻辑
  • 与 Mock 的区别——spy 执行真实代码,mock 用存根替换它
  • Mockito.spy()——适用于 Java/Kotlin,包装真实对象,支持 verify 和 partial mock
  • MockK.spyk()——Kotlin 对应方案,支持协程、密封类和 coVerify
  • iOS——OCMock 用于 Objective-C,手动 protocol-based spy 用于 Swift
  • 主要场景——检查分析、定时器、日志和调用顺序
  • 注意——具有 I/O 操作的 spy 将单元测试变成集成测试

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读