Test Doubles là các đối tượng thay thế được sử dụng trong kiểm thử đơn vị thay cho các phụ thuộc thực tế. Thuật ngữ này được Gerard Meszaros giới thiệu trong cuốn sách “xUnit Test Patterns” (2007) như một khái niệm chung cho Mock, Stub, Fake, Spy và Dummy. Theo Martin Fowler (2024), Test Doubles cho phép cô lập thành phần đang được kiểm thử khỏi môi trường của nó, làm cho các bài kiểm thử trở nên xác định, nhanh chóng và độc lập với các dịch vụ bên ngoài.
Những điểm chính
Test Doubles là một thuật ngữ từ ngành công nghiệp ô tô (diễn viên đóng thế), được chuyển sang phát triển phần mềm. Giống như diễn viên đóng thế thay thế diễn viên chính trong một cảnh nguy hiểm, Test Double thay thế một thành phần thực tế trong kịch bản kiểm thử. Điều này cần thiết khi phụ thuộc thực tế không khả dụng, chậm, không xác định hoặc có tác dụng phụ.
Khái niệm Test Double bao gồm năm loại cụ thể, mỗi loại giải quyết nhiệm vụ riêng của nó. Phân loại của Meszaros là chuẩn mực và được sử dụng trong tất cả các hướng dẫn kiểm thử hiện đại. Sự khác biệt giữa các loại nằm ở mức độ kiểm soát và xác minh: từ việc điền tham số đơn giản (Dummy) đến xác minh hoàn chỉnh chuỗi lời gọi (Mock).
Mục đích chính của Test Doubles là cô lập mô-đun đang được kiểm thử. Trong phát triển di động, các phụ thuộc thực tế bao gồm máy chủ API, cơ sở dữ liệu, hệ thống tệp, cảm biến thiết bị và dịch vụ hệ thống (LocationManager, Camera, Bluetooth). Sử dụng trực tiếp các thành phần này làm cho các bài kiểm thử chậm, dễ vỡ và phụ thuộc vào môi trường. Theo Google Testing Blog (2023), các bài kiểm thử đơn vị được cô lập tốt chạy trong miligiây, trong khi kiểm thử tích hợp chạy trong vài giây và phút.
Phân loại của Gerard Meszaros bao gồm năm loại Test Doubles, khác nhau về hành vi và mục đích. Hiểu sự khác biệt giữa chúng là nền tảng của kiểm thử đơn vị thành thạo.
Dummy là một đối tượng được truyền vào phương thức đang kiểm thử nhưng không bao giờ được sử dụng. Dummy chỉ cần thiết để đáp ứng chữ ký phương thức. Trong Kotlin, đây thường là null, emptyList() hoặc một đối tượng với các stub. Dummy không được chứa bất kỳ logic nào — nếu nó được gọi, bài kiểm thử phải thất bại.
Fake là một triển khai đơn giản nhưng hoạt động được của một interface. Không giống như Mock và Stub, Fake chứa logic nghiệp vụ thực tế, nhưng ở dạng đơn giản hóa. Một ví dụ kinh điển là InMemoryUserRepository, lưu trữ dữ liệu trong HashMap thay vì cơ sở dữ liệu. Fake được sử dụng khi bạn cần kiểm thử logic phụ thuộc vào trạng thái, nhưng không có chi phí của cơ sở hạ tầng thực tế.
| Loại | Mục đích | Ví dụ |
|---|---|---|
| Dummy | Điền tham số | null, đối tượng rỗng |
| Fake | Triển khai đơn giản hoạt động | InMemoryRepository |
| Stub | Trả về giá trị cố định | when(api.getUser()).thenReturn(user) |
| Spy | Ghi lại lời gọi để xác minh | verify(spy).save(user) |
| Mock | Xác minh tương tác | verify(mock).sendEmail(email) |
Stub trả về các giá trị được xác định trước cho các lời gọi cụ thể. Stub không kiểm tra xem nó có được gọi hay không — nó chỉ đơn giản cung cấp dữ liệu. Trong Mockito, Stub được tạo qua when(method).thenReturn(value). Stub lý tưởng cho kiểm thử khi bạn cần một phụ thuộc trả về một giá trị cụ thể, nhưng bản thân việc gọi không quan trọng.
Spy là một lớp bọc xung quanh một đối tượng thực tế, ghi lại tất cả các lời gọi để xác minh sau. Không giống như Mock, Spy ủy quyền các lời gọi cho đối tượng thực tế nhưng cho phép xác minh rằng chúng đã xảy ra. Trong Mockito, Spy được tạo qua spy(realObject). Spy hữu ích cho mocking một phần, khi bạn muốn sử dụng đối tượng thực tế nhưng xác minh một số lời gọi.
Mock là một đối tượng với các kỳ vọng lời gọi được xác định trước. Mock xác minh rằng các phương thức cụ thể đã được gọi với các đối số cụ thể và theo một thứ tự cụ thể. Không giống như Stub, Mock tập trung vào xác minh hành vi thay vì trả về dữ liệu. Mock là loại Test Double mạnh mẽ nhất và được sử dụng thường xuyên nhất trong phát triển di động.
Sự khác biệt giữa Mock và Stub thường gây nhầm lẫn ngay cả với các nhà phát triển giàu kinh nghiệm. Sự khác biệt chính nằm ở mục đích: Stub xác minh trạng thái (state verification), Mock xác minh hành vi (behavior verification).
Stub trả lời câu hỏi: “mã có trả về kết quả chính xác không?”. Mock trả lời câu hỏi: “mã có gọi đúng phương thức với đúng đối số không?”. Trong phát triển di động, Stub được sử dụng khi kết quả quan trọng (ví dụ: dữ liệu từ kho lưu trữ), trong khi Mock được sử dụng khi tác dụng phụ quan trọng (ví dụ: gửi email, ghi vào cơ sở dữ liệu).
// Stub: xác minh trạng thái
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: xác minh hành vi
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Các ví dụ thực tế về tất cả năm loại Test Doubles trong Kotlin sử dụng MockK — thư viện mocking phổ biến nhất cho các dự án Android.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: trả về phản hồi API cố định
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: xác minh rằng người dùng đã được lưu
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context không được sử dụng bên trong Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
Việc chọn loại Test Double phụ thuộc vào những gì đang được kiểm thử: trạng thái, hành vi hay tích hợp. Trong phát triển di động Android và iOS, các khuyến nghị sau đã được thiết lập.
Khi kiểm thử ViewModel, hãy sử dụng Mock cho các phụ thuộc tạo ra tác dụng phụ (kho lưu trữ, analytics, điều hướng) và Stub cho các phụ thuộc trả về dữ liệu (máy khách API, ContentProvider). Điều này cho phép xác minh rằng ViewModel xử lý chính xác cả kịch bản thành công và lỗi.
Ở cấp độ Repository, hãy ưu tiên Fake (triển khai cơ sở dữ liệu trong bộ nhớ) và Stub (phản hồi API cố định). Fake cho phép kiểm thử logic bộ nhớ đệm và chế độ ngoại tuyến mà không cần thiết lập SQLite. Stub mô phỏng các trạng thái HTTP khác nhau: 200, 404, 500, timeout.
Sử dụng không đúng Test Doubles là một trong những nguyên nhân phổ biến nhất của các kiểm thử dễ vỡ, hỏng sau mỗi lần tái cấu trúc.
Lỗi phổ biến nhất là mock mọi thứ. Nếu mọi phụ thuộc trong một bài kiểm thử được thay thế bằng Mock, bài kiểm thử ngừng xác minh hành vi thực tế. Mock chỉ nên được sử dụng cho các phụ thuộc bên ngoài (mạng, cơ sở dữ liệu, hệ thống tệp, dịch vụ hệ thống). Các thành phần nội bộ của ứng dụng (Value Object, data class, tiện ích đơn giản) không nên được thay thế.
Lỗi thứ hai là tạo Mock mà không xác định kỳ vọng. Nếu một phương thức được gọi mà không có every / when, Mock trả về giá trị mặc định (null, 0, false). Điều này có thể dẫn đến các kiểm thử dương tính giả, nơi Mock âm thầm trả về null và bài kiểm thử diễn giải điều này là hành vi đúng.
Lỗi thứ ba là xác minh mọi lời gọi của mọi Mock. Verify chỉ nên được sử dụng cho các lời gọi quan trọng từ góc độ logic nghiệp vụ. Xác minh quá mức làm cho các kiểm thử dễ vỡ: thay đổi thứ tự lời gọi trong mã sản xuất làm hỏng kiểm thử mà không thay đổi hành vi.
Câu hỏi thường gặp
Stub trả về dữ liệu và xác minh trạng thái (những gì được trả về), trong khi Mock xác minh hành vi (phương thức nào đã được gọi). Stub = “trả về X”, Mock = “xác minh rằng Y đã được gọi với đối số Z”. Trong các bài kiểm thử thực tế, một đối tượng thường hoạt động đồng thời cả Stub và Mock.
Fake được ưu tiên hơn Mock khi kiểm thử logic phụ thuộc vào trạng thái: bộ nhớ đệm, chế độ ngoại tuyến, giao dịch. Fake (triển khai trong bộ nhớ) cho phép kiểm thử các kịch bản này mà không cần các lời gọi verify dễ vỡ. Mock phù hợp hơn để xác minh gửi dữ liệu: analytics, push, email.
Đối với các dự án Android bằng Kotlin, MockK được khuyến nghị. Nó hỗ trợ coroutine, hàm suspend, sealed class và hàm mở rộng mà không cần cấu hình thêm. Đối với các dự án Java, Mockito vẫn là tiêu chuẩn — thư viện phổ biến nhất với tài liệu phong phú.
Để kiểm thử Kotlin Flow, hãy sử dụng thư viện Turbine cùng với MockK. Turbine đơn giản hóa việc xác minh phát xạ Flow: bạn có thể xác minh thứ tự giá trị, hoàn thành luồng và ngoại lệ. Stub cho Flow trả về flowOf(value), Mock xác minh rằng Flow đã được thu thập.
Có, nhưng ở mức phản hồi API, không phải thành phần UI. Các thư viện MockWebServer (OkHttp) và WireMock cho phép mô phỏng phản hồi HTTP trong kiểm thử UI. Bản thân các thành phần UI (Compose, SwiftUI Views) không nên được thay thế — hành vi của chúng được kiểm thử qua kiểm thử ảnh chụp màn hình và Espresso.
Tóm tắ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