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 — 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ử.
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.
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ản | Spy | Mock |
|---|---|---|
| Logic thực tế cần thiết | Có | Không (stub) |
| Xác minh lời gọi | Có (số lượng, đối số) | Có (số lượng, đối số) |
| Stubbing một phần | Có (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 đọc | Thấ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) |
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() — 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”.
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.
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 — 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.
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.
// 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.
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.
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 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 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ỡ.
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.
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.
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
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.
Đọc thêm