MockK là một framework Kotlin-first để tạo các đối tượng mock, được thiết kế đặc biệt cho hệ sinh thái Kotlin với các đặc điểm ngôn ngữ của nó: coroutine, hàm mở rộng, data class và sealed class. Không giống Mockito được chuyển từ Java sang Kotlin, MockK được thiết kế ngay từ đầu cho cú pháp Kotlin và không yêu cầu plugin bổ sung để làm việc với các lớp final. Theo MockK.io, thư viện được sử dụng trong hơn 40% các dự án Kotlin có kiểm thử đơn vị.
Những điểm chính
MockK là một thư viện để tạo các đối tượng mock, được viết bằng Kotlin và tối ưu hóa cho cú pháp của nó. Nó giải quyết các vấn đề tương tự như Mockito — cô lập mã được kiểm thử khỏi các phụ thuộc — nhưng thực hiện điều đó bằng cách sử dụng các cấu trúc đặc thù của Kotlin: lambda, DSL, reified generics và các hàm suspend.
Lợi thế chính của MockK so với các giải pháp được chuyển đổi là hỗ trợ Kotlin gốc. Trong Mockito, việc mock một lớp final yêu cầu opt-in (mockito-inline) và các phương thức tĩnh yêu cầu mockStatic. MockK hỗ trợ điều này theo mặc định, vì các lớp Kotlin là final theo mặc định và việc vượt qua giới hạn này được tích hợp trong kiến trúc của thư viện.
Phiên bản 1.13.12 (2024) là bản phát hành ổn định hỗ trợ Kotlin 2.0, trình biên dịch K2 và các dự án đa nền tảng (KMP). MockK cũng hoạt động với Kotlin/Native và Kotlin/JS, khiến nó trở thành lựa chọn duy nhất cho các dự án KMP nơi cả Mockito và EasyMock đều không áp dụng được.
MockK được thiết kế có tính đến các đặc thù của Kotlin và sử dụng các tính năng ngôn ngữ — reified generics, DSL với lambda, hàm inline — để cung cấp API ngắn gọn và type-safe mà không hy sinh hiệu suất.
Cơ chế của MockK dựa trên việc instrument bytecode thông qua thư viện ByteBuddy (giống Mockito), nhưng bọc nó trong một DSL thân thiện với Kotlin. Thay vì chuỗi when().thenReturn(), MockK sử dụng các khối lambda every { } và coEvery { } trông giống như một phần mở rộng tự nhiên của ngôn ngữ. Bên trong, MockK chặn cuộc gọi bên trong lambda, phân tích phương thức và tham số thông qua reflection và đối sánh chúng với các quy tắc stubbing đã ghi lại.
Khối every { mock.method() } returns value được đọc là “mỗi khi phương thức được gọi, trả về giá trị.” Cú pháp khai báo này gần với phong cách Kotlin hơn và loại bỏ sự nhầm lẫn về thứ tự tham số trong when(). Nhờ reified generics của Kotlin, kiểu của mock được suy luận tự động mà không cần chỉ định lớp một cách rõ ràng.
val repository = mockk<UserRepository>()
// Stubbing: mỗi lần gọi findById(1) trả về người dùng
every { repository.findById(1) } returns User("Alice")
// Cuộc gọi và xác minh
val result = repository.findById(1)
assertEquals("Alice", result.name)
Không giống Mockito, nơi mỗi phương thức phải được cấu hình rõ ràng, MockK hỗ trợ relaxed mock — một mock trả về các giá trị mặc định “hợp lý” cho bất kỳ phương thức nào: danh sách rỗng cho List, 0 cho Int, chuỗi rỗng cho String. Điều này giảm đáng kể lượng mã thiết lập.
// Relaxed mock — tất cả phương thức trả về giá trị mặc định
val api = mockk<ApiService>(relaxed = true)
// Không yêu cầu stubbing — trả về danh sách rỗng
println(api.getUsers()) // []
MockK cung cấp một số cách để tạo đối tượng mock: mockk<T>() cho mock nghiêm ngặt (mỗi phương thức phải được cấu hình rõ ràng), mockk<T>(relaxed = true) cho mock relaxed và spyk(obj) để tạo spy trên một đối tượng thực.
| Hàm | Loại | Hành vi không có stubbing |
|---|---|---|
| mockk() | Mock nghiêm ngặt | Ném ngoại lệ khi gọi phương thức chưa được stub |
| mockk(relaxed = true) | Mock relaxed | Trả về giá trị mặc định |
| spyk() | Spy | Gọi phương thức thực nếu không có stub nào được cấu hình |
| slot() | Argument Captor | Chụp tham số để xác minh |
Việc lựa chọn giữa mock nghiêm ngặt và relaxed phụ thuộc vào ngữ cảnh. Mock nghiêm ngặt đảm bảo rằng bài kiểm thử không sử dụng các phương thức có hành vi không được xác định — điều này tăng độ tin cậy. Mock relaxed thuận tiện cho việc tạo nguyên mẫu kiểm thử nhanh khi không phải tất cả các phụ thuộc đều quan trọng. Trong thực tế, nên bắt đầu với mock nghiêm ngặt và chỉ chuyển sang relaxed khi stubbing chiếm nhiều dòng hơn chính bài kiểm thử.
Khối every là cấu trúc stubbing trung tâm trong MockK. Bên trong lambda, một lệnh gọi phương thức với các tham số cụ thể được mô tả, sau đó một giá trị được trả về qua returns, một ngoại lệ được ném qua throws hoặc một phản hồi được tính toán qua answers.
MockK hỗ trợ tất cả các kịch bản cần thiết cho kiểm thử: trả về giá trị, ném ngoại lệ, tính toán phản hồi dựa trên tham số và nhiều phản hồi theo thứ tự (chuỗi cuộc gọi).
// Trả về giá trị
every { repo.findById(1) } returns User("Alice")
// Ném ngoại lệ
every { repo.findById(999) } throws NotFoundException()
// Phản hồi động
every { repo.save(any()) } answers {
val user = firstArg<User>()
user.copy(id = 42)
}
// Chuỗi phản hồi
every { repo.findAll() } returnsMany listOf(
listOf(User("Alice")),
listOf(User("Bob")),
emptyList()
)
Verify trong MockK tương tự Mockito.verify() về mục đích, nhưng sử dụng DSL Kotlin: verify { mock.method() }. Đối với các hàm suspend, sử dụng coVerify { mock.suspendMethod() }, hoạt động chính xác với coroutine và không yêu cầu trình chạy đặc biệt.
MockK hỗ trợ các bổ ngữ giống như Mockito: exactly(1), atLeast(2), atMost(5), wasNot(Called). Cú pháp tối giản — bổ ngữ được truyền làm tham số đầu tiên trong verify { }.
// Xác minh: phương thức được gọi đúng 1 lần
verify(exactly = 1) { repo.save(any()) }
// Xác minh thứ tự cuộc gọi
verifySequence {
repo.save(any())
repo.flush()
}
// coVerify cho hàm suspend
coVerify { api.fetchUsers() }
Để xác minh tham số, sử dụng slot() — tương tự như ArgumentCaptor. Slot được khai báo trước khi gọi, truyền vào every hoặc verify, và sau khi thực thi kiểm thử chứa giá trị đã chụp.
val userSlot = slot<User>()
verify { repo.save(capture(userSlot)) }
assertEquals("Alice", userSlot.captured.name)
MockK cung cấp các chú thích @MockK và @RelaxedMockK để tạo mock thông qua khởi tạo trong JUnit 5. Phần mở rộng MockKExtension tự động tạo mock trước mỗi bài kiểm thử và dọn dẹp sau đó — tương tự MockitoExtension, nhưng có hỗ trợ chế độ relaxed.
Chú thích @InjectMockKs (hoặc thay thế @MockK với tạo đối tượng rõ ràng) tiêm mock vào thể hiện đang được kiểm thử. Điều này giảm boilerplate và làm cho mã kiểm thử sạch hơn.
@ExtendWith(MockKExtension::class)
class UserServiceTest {
@MockK
lateinit var repository: UserRepository
@InjectMockKs
lateinit var service: UserService
@Test
fun `getUser returns user from repository`() {
every { repository.findById(1) } returns User("Alice")
assertEquals("Alice", service.getUser(1)?.name)
}
}
Lựa chọn giữa MockK và Mockito phụ thuộc vào thành phần nhóm và loại dự án. Mockito có hệ sinh thái lớn hơn, nhiều ví dụ và tích hợp hơn, nhưng MockK cung cấp cú pháp Kotlin sạch hơn và hỗ trợ gốc cho các tính năng ngôn ngữ. Đối với các dự án Kotlin mới, MockK được khuyến nghị như một giải pháp mang tính thành ngữ hơn.
| Tiêu chí | MockK | Mockito |
|---|---|---|
| Cú pháp | DSL Kotlin (every, verify) | Phong cách Java (when, thenReturn) |
| Coroutine | coEvery, coVerify (gốc) | Yêu cầu thư viện bổ sung |
| Lớp final | Hỗ trợ theo mặc định | Yêu cầu mockito-inline |
| KMP | Được hỗ trợ | Không được hỗ trợ |
| Relaxed mock | Tích hợp sẵn | Không có tương đương |
| Phổ biến | Đang tăng trong cộng đồng Kotlin | Chiếm ưu thế trong các dự án Java và lai |
Đối với các dự án Kotlin thuần túy (không có lớp Java), MockK được ưu tiên hơn: ít boilerplate hơn, hỗ trợ coroutine gốc, không có bất ngờ với lớp final. Đối với các dự án lai hoặc nhóm có nền tảng Java, Mockito vẫn là một lựa chọn khả thi — cả hai thư viện có thể được sử dụng trong cùng một dự án thông qua các mô-đun khác nhau. Khi di chuyển từ Mockito sang MockK, chỉ cần thay thế chú thích @Mock bằng @MockK và viết lại các khối when().thenReturn() sang định dạng every { }.
Các câu hỏi thường gặp
Relaxed mock trả về giá trị mặc định cho tất cả các phương thức chưa được stub (danh sách rỗng, 0, null) mà không ném ngoại lệ. Mock thông thường (nghiêm ngặt) yêu cầu stubbing rõ ràng cho mỗi phương thức — nếu không bài kiểm thử sẽ thất bại. Relaxed mock thuận tiện cho kiểm thử nhanh, nghiêm ngặt cho kiểm thử đáng tin cậy.
MockK hỗ trợ mocking các hàm mở rộng thông qua mockkStatic(). Điều này có thể thực hiện được vì các hàm mở rộng trong Kotlin là các phương thức tĩnh với receiver là tham số đầu tiên. Đối với mỗi hàm mở rộng, bạn cần chỉ định lớp mà nó được khai báo.
Có, MockK hỗ trợ Kotlin Multiplatform (KMP) cho mã chung. Trên các nền tảng JVM, Native và JS, bạn có thể sử dụng API chung: mockk(), every, verify. Điều này làm cho MockK trở thành lựa chọn duy nhất cho các dự án KMP nơi Mockito không hoạt động.
Sử dụng verifySequence { } — một khối nơi các cuộc gọi được chỉ định theo đúng thứ tự dự kiến. Nếu thứ tự thực tế khác, verifySequence sẽ ném ngoại lệ cho biết cuộc gọi không khớp đầu tiên.
Có, về mặt kỹ thuật là có thể, nhưng không được khuyến nghị. Xung đột có thể xảy ra ở cấp độ instrument bytecode (ByteBuddy vs mockito-inline). Nếu dự án đã sử dụng Mockito, việc di chuyển sang MockK có thể thực hiện dần dần thông qua cách ly mô-đun.
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