Spy — nó là gì, Mockito.spy() và xác minh lời gọi

Tác giả: IT Sectr Đã đăng: 2026-04-10 Thời gian đọc: 9 phút

Spy (gián điệp) — một đối tượng kiểm thử bao bọc một thể hiện thực tế và ghi lại thông tin về mỗi lời gọi: phương thức nào được gọi, với đối số nào và bao nhiêu lần. Không giống mock, spy sử dụng trình triển khai thực tế của đối tượng được bao bọc — lời gọi đi qua mã thực tế và spy chỉ ghi lại sự kiện. Sau khi kiểm thử được thực hiện, nhà phát triển kiểm tra bản ghi của gián điệp: “Phương thức sendAnalytics có được gọi ba lần không?”. Tìm hiểu thêm trong hướng dẫn kiểm thử Android.

Ý chính

  • Spy — đối tượng ghi lại lời gọi phương thức của một trình triển khai thực tế mà không thay thế nó
  • Xác minh — sau kiểm thử, spy cho phép kiểm tra bao nhiêu lần và với đối số nào một phương thức được gọi
  • Mockito.spy() — tạo gián điệp trên Android cho các đối tượng Java/Kotlin thực tế
  • Mocking một phần — spy có thể kết hợp với stub: chặn một số phương thức, để các phương thức khác đi qua
  • iOS — OCMock (Objective-C) và gián điệp thủ công qua giao thức trong Swift

Spy là gì và khác gì với Mock?

Spy — một lớp bọc quanh đối tượng thực tế chặn tất cả các lời gọi phương thức và ghi lại chúng. Logic thực tế của đối tượng được thực thi: nếu phương thức lưu dữ liệu, tính toán giá trị hoặc thực hiện yêu cầu — mọi thứ diễn ra như bình thường. Ngoài ra, spy ghi lại siêu dữ liệu: tên phương thức, đối số, số lần gọi, thời gian thực thi. Thuật ngữ này là một phần trong phân loại Meszaros (2007) và được mô tả chi tiết trong bài viết “Mocks Aren’t Stubs” của Martin Fowler.

Sự khác biệt chính với Mock — mock thay thế hoàn toàn đối tượng bằng một stub kiểm thử; tất cả các phương thức mặc định không làm gì. Spy bao bọc một đối tượng hiện có: tất cả các phương thức mặc định hoạt động như bình thường, nhưng cũng được ghi lại. Sự khác biệt này là cơ bản: mock cô lập mã được kiểm thử khỏi thực tế, trong khi spy bảo tồn thực tế và cho phép quan sát nó. Lựa chọn giữa chúng phụ thuộc vào những gì đang được kiểm thử.

Khi nào Spy là lựa chọn đúng

Spy là lựa chọn đúng — nếu mã được kiểm thử sửa đổi trạng thái của một đối tượng thực tế và kiểm thử phải vừa xác minh kết quả (trạng thái) vừa đảm bảo rằng các lời gọi được thực hiện theo đúng thứ tự. mock không phù hợp vì nó không thực thi trình triển khai thực tế. stub không phù hợp vì nó không ghi lại lời gọi. Spy là test double duy nhất vừa bảo tồn logic thực tế vừa cung cấp thông tin về các lời gọi.

Spy vs Mock: khi nào sử dụng gián điệp

Mock — cô lập hoàn toàn. Nếu kiểm thử không nên phụ thuộc vào trình triển khai của đối tượng thực tế (ví dụ, cơ sở dữ liệu hoặc trình khách mạng), hãy sử dụng mock. mock đảm bảo rằng không có lời gọi nào đến được thành phần thực tế. Điều này an toàn và có thể dự đoán trước. Nhược điểm: mock không thực thi logic thực tế, vì vậy nếu mã được kiểm thử phụ thuộc vào giá trị trả về, nó phải được cấu hình rõ ràng qua when/stub.

Spy — logic thực tế + quan sát. Nếu mã được kiểm thử tương tác với một đối tượng mà logic của nó quan trọng đối với kiểm thử chứ không chỉ dữ liệu — hãy sử dụng spy. Ví dụ, AnalyticsTracker thu thập sự kiện và gửi chúng định kỳ. Kiểm thử xác minh rằng các sự kiện đã được thêm vào bộ đệm và sau khi gửi, bộ đệm đã được xóa. mock không thể xác minh điều này vì nó không thực thi logic thực tế của trình theo dõi.

Kịch bảnSpyMock
Logic thực tế cần thiếtKhông (stub)
Xác minh lời gọiCó (số lượng, đối số)Có (số lượng, đối số)
Stubbing một phầnCó (một số phương thức spy, số khác stub)Không (tất cả phương thức đều là stub)
Rủi ro tác dụng phụCao (mã thực tế)Không
Tốc độThấp hơn (logic thực tế)Cao hơn (stub)
Khả năng đọcThấp hơn (khó hiểu hơn cái gì là thực tế)Cao hơn (mọi thứ đều rõ ràng)

Phản mẫu: spy cho mọi thứ

Spy cho mọi thứ — sử dụng spy thay vì mock cho tất cả các kiểm thử là một sai lầm. spy thực thi mã thực tế có thể có tác dụng phụ: ghi vào tệp, gửi HTTP, thay đổi trạng thái toàn cục. Nếu mô-đun được kiểm thử gọi một phương thức của đối tượng spy thực hiện yêu cầu HTTP, kiểm thử trở thành kiểm thử tích hợp chứ không phải kiểm thử đơn vị. Quy tắc: nếu spy bao bọc một đối tượng có thao tác I/O — thì đó không còn là kiểm thử đơn vị nữa. Sử dụng mock để cô lập I/O và chỉ sử dụng spy cho các đối tượng trong bộ nhớ không có tác động bên ngoài.

Mockito.spy() và spy trong MockK trên Android

Mockito.spy() — cách cổ điển để tạo gián điệp trong các dự án Java/Kotlin. spy() nhận một đối tượng thực tế và trả về một lớp bọc. Tất cả các lời gọi mặc định được ủy quyền cho đối tượng thực tế và kết quả được ghi lại. Sau kiểm thử, bạn có thể sử dụng verify() để kiểm tra số lượng lời gọi và đối số. Đối với các phương thức sẽ trả về dữ liệu kiểm thử, sử dụng doReturn/when — điều này được gọi là “mocking một phần”.

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() — một lựa chọn thay thế cho các dự án Kotlin với hỗ trợ coroutine và lớp sealed tốt hơn. MockK.spyk() tạo một gián điệp, tương tự Mockito.spy(). Nó hỗ trợ coVerify cho các hàm suspend và every cho stubbing một phần. Không giống Mockito, MockK không hỗ trợ spy cho các lớp final (tất cả các lớp trong Kotlin đều là final mặc định) — bạn cần mở lớp (open) hoặc sử dụng 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)
    }
}

Stubbing một phần qua Spy

Stubbing một phần — một kỹ thuật mạnh mẽ nhưng nguy hiểm. Bạn có thể tạo spy của một đối tượng và ghi đè (stub) chỉ một số phương thức, để lại các phương thức khác là thực tế. Ví dụ: một kho lưu trữ spy nơi getUser() trả về dữ liệu kiểm thử, trong khi saveUser() thực sự lưu vào danh sách trong bộ nhớ. Điều này cho phép kết hợp lợi ích của stub (dữ liệu được kiểm soát) và spy (logic thực tế). Nhược điểm: khả năng đọc kiểm thử giảm — không rõ phương thức nào là thực tế và phương thức nào là stub.

Triển khai Spy trên iOS với OCMock và giao thức

OCMock cho Objective-C — một thư viện hỗ trợ tạo các đối tượng spy qua niceMock. OCMock chặn các lời gọi phương thức bằng runtime Objective-C và ghi lại chúng. Sau kiểm thử, verify được gọi. OCMock hỗ trợ spy cho bất kỳ đối tượng nào (tất cả các phương thức trong Objective-C đều động), điều này mang lại lợi thế so với Swift, nơi spy chỉ khả thi qua giao thức.

objective-c
// Tạo spy cho một đối tượng thực tế
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];

// Chạy kiểm thử
[spy trackEvent:@"login"];

// Xác minh
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);

Spy dựa trên giao thức trong Swift — Swift không có phản chiếu runtime như Objective-C, vì vậy spy được tạo thủ công. Một cấu trúc kiểm thử triển khai một giao thức và bên trong gọi đối tượng thực tế đồng thời ghi lại lời gọi. Điều này cần nhiều mã hơn nhưng hoàn toàn có thể kiểm soát được và an toàn về kiểu. Các spy thủ công không yêu cầu thư viện bên ngoài và không sử dụng runtime — mọi thứ được kiểm tra tại thời gian biên dịch.

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

Khi nào sử dụng OCMock so với spy thủ công — đối với mã Objective-C, sử dụng OCMock (ít boilerplate hơn). Đối với Swift, nên sử dụng spy thủ công qua giao thức. Spy thủ công cho phép kiểm soát hoàn toàn việc ghi lại lời gọi, không yêu cầu phản chiếu và hoạt động với các kiểu giá trị (struct). Nhược điểm duy nhất là bạn cần duy trì mã lớp spy đồng bộ với giao thức khi thêm các phương thức mới.

Các kịch bản sử dụng Spy điển hình

Xác minh phân tích — trường hợp sử dụng phổ biến nhất cho spy. Trong mã sản xuất, các lời gọi phân tích nằm rải rác khắp ứng dụng: đăng nhập, đăng xuất, mua hàng, lỗi. Kiểm thử tạo một lớp bọc spy cho AnalyticsTracker, thực thi một kịch bản (đăng nhập, xem sản phẩm, thêm vào giỏ hàng, mua) và kiểm tra xem tất cả các sự kiện cần thiết đã được gửi theo đúng thứ tự chưa. mock không phù hợp vì AnalyticsTracker chứa logic đệm và gửi.

Bộ đếm thời gian và bộ lập lịch — kiểm thử mã sử dụng Handler (Android) hoặc Timer (iOS) rất khó vì thời gian thực. Một lớp bọc spy cho Scheduler ghi lại những tác vụ nào được lên lịch và với độ trễ nào. Kiểm thử tạo spy của Handler thực tế, thực hiện một hành động và kiểm tra xem Handler.postDelayed(runnable, delay) có được gọi với độ trễ chính xác không. Tác vụ thực tế không được thực thi — spy chặn và ghi lại lời gọi.

Ghi log và thông tin gỡ lỗi — trong sản xuất, log có thể bị tắt hoặc ghi vào tệp. Một lớp bọc spy cho Logger ghi lại tất cả thông báo vào danh sách trong bộ nhớ mà kiểm thử sẽ kiểm tra sau khi thực thi. Điều này cho phép xác minh rằng thông báo chính xác đã được ghi khi có lỗi mà không làm xộn bãng điều khiển. Spy thủ công cho Logger đặc biệt hữu ích trên iOS, nơi OSLog không có API kiểm thử.

Xác minh thứ tự lời gọi — một số kịch bản yêu cầu thứ tự thao tác nghiêm ngặt: mở kết nối, gửi dữ liệu, đóng kết nối. Mockito cho phép kiểm tra thứ tự qua InOrder.verify(). spy cũng làm điều tương tự nhưng bảo tồn thực thi thực tế. Nếu cả thứ tự và kết quả của từng bước (kết nối thực sự đã được mở) đều quan trọng — hãy sử dụng spy, không phải mock.

Câu hỏi thường gặp

Spy vs Mock: sự khác biệt chính là gì?

Spy bao bọc một đối tượng thực tế và thực thi logic của nó, đồng thời ghi lại lời gọi. Mock thay thế hoàn toàn đối tượng bằng stub — không có logic thực tế nào được thực thi. spy bảo tồn hành vi, mock thì không. Chọn spy khi công việc thực tế của đối tượng quan trọng; chọn mock khi bạn cần cô lập kiểm thử khỏi sự phụ thuộc bên ngoài.

Khi nào Spy là một lựa chọn tồi?

Khi lớp bọc spy dẫn đến các thao tác I/O thực tế. Nếu spy bao bọc một đối tượng ghi vào tệp, gửi HTTP hoặc đọc từ đĩa — kiểm thử không còn là kiểm thử đơn vị nữa. Trường hợp thứ hai: kiểm thử chỉ xác minh giá trị trả về mà không quan tâm đến lời gọi — ở đây stub là đủ và spy là thừa. Thứ ba: mã phụ thuộc vào trạng thái nội bộ của spy — đây là một kiểm thử dễ vỡ.

MockK có hỗ trợ spy không?

Có, qua spyk() — tương đương với Mockito.spy(). MockK.spyk() tạo một gián điệp quanh đối tượng thực tế, hỗ trợ every cho stubbing một phần và coVerify/coroutinesVerify cho các hàm suspend. Giới hạn: không hoạt động với các lớp final (cần open hoặc interface). Đối với các lớp Java, MockK cũng hỗ trợ spyk() nhưng yêu cầu chú thích @MockKJvmInline.

Có thể tạo Spy từ Mock không?

Về mặt kỹ thuật, không. Mock là stub không chứa trình triển khai thực tế. Spy, theo định nghĩa, bao bọc một đối tượng thực tế. Trong Mockito, bạn không thể biến mock thành spy. Nhưng bạn có thể làm ngược lại: tạo spy và ghi đè một số phương thức qua doReturn/when (mocking một phần). Điều này tạo ra hành vi giống mock cho các phương thức được chọn của đối tượng spy.

Spy trong Swift có bắt buộc phải có giao thức không?

Có, bắt buộc. Trong Swift, không có proxy động như trong Java/Kotlin. Để tạo spy, cần một giao thức mà cả lớp sản xuất và lớp spy đều triển khai. Spy dựa trên giao thức trong Swift là một triển khai thủ công nhận một đối tượng thực tế, ủy quyền lời gọi cho nó và ghi lại siêu dữ liệu. Lựa chọn thay thế: thư viện Cuckoo, tạo các lớp spy qua SourceKit.

Tổng kết

  • Spy — lớp bọc quanh đối tượng thực tế ghi lại tất cả lời gọi phương thức mà không thay thế logic
  • Khác biệt với Mock — spy thực thi mã thực tế, mock thay thế nó bằng stub
  • Mockito.spy() — cho Java/Kotlin, bao bọc đối tượng thực tế với hỗ trợ verify và partial mock
  • MockK.spyk() — tương đương Kotlin với hỗ trợ coroutine, lớp sealed và coVerify
  • iOS — OCMock cho Objective-C, spy thủ công dựa trên giao thức cho Swift
  • Kịch bản chính — xác minh phân tích, bộ đếm thời gian, log và thứ tự lời gọi
  • Thận trọng — spy với thao tác I/O biến kiểm thử đơn vị thành kiểm thử tích hợp

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm