Mock — nó là gì, đối tượng mock và thư viện kiểm thử

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

Mock là một đối tượng thay thế mô phỏng hành vi của một thành phần thực và cho phép kiểm tra tương tác với nó. Không giống như Stub, chỉ đơn giản trả về một giá trị định trước, Mock ghi lại thực tế của việc gọi phương thức, các tham số được truyền và số lần gọi. Theo Mockito (2024), Mock là loại Test Double phổ biến nhất trong các dự án Java và Kotlin, được sử dụng trong hơn 70% bài kiểm thử đơn vị của ứng dụng di động.

Những điểm chính

  • Mock — một đối tượng kiểm tra tương tác: phương thức nào được gọi, với tham số nào và bao nhiêu lần
  • Mockito — thư viện phổ biến nhất để tạo Mock trong các dự án Java và Android
  • MockK — một thay thế cho Mockito dành cho Kotlin với hỗ trợ gốc cho coroutine và sealed class
  • Behavior verification — sự khác biệt chính giữa Mock và Stub: Mock kiểm tra hành vi, không phải trạng thái
  • Over-mocking — phản mẫu chính: mock chỉ nên được sử dụng cho các phụ thuộc bên ngoài

Mock là gì?

Mock là một đối tượng được tạo bởi framework mocking (Mockito, MockK, EasyMock) mô phỏng một interface hoặc class và ghi lại tất cả các lệnh gọi đến phương thức của nó. Nhà phát triển đặt kỳ vọng: phương thức X sẽ được gọi với tham số Y và trả về Z. Sau khi kiểm thử thực thi, Mock xác minh rằng các kỳ vọng khớp với các lệnh gọi thực tế.

Thuật ngữ này xuất phát từ phép ẩn dụ sân khấu của Test Doubles: Mock là một “người đóng giả” không chỉ đứng trên sân khấu (như Dummy) mà còn thực hiện một vai trò và kiểm tra xem tương tác với nó có đúng không. Nếu mã được kiểm thử không gọi phương thức mà Mock mong đợi, hoặc gọi với tham số không chính xác — kiểm thử thất bại với thông báo kỳ vọng bị vi phạm.

Cách Mock hoạt động

Mock được tạo thông qua factory của framework: mockk<MyInterface>() hoặc Mockito.mock(MyClass.java). Framework tạo ra một đối tượng proxy chặn tất cả các lệnh gọi phương thức. Mỗi lệnh gọi được so sánh với các kỳ vọng được xác định trước. Nếu lệnh gọi khớp với kỳ vọng — giá trị được chỉ định được trả về. Nếu không — Mock trả về giá trị mặc định hoặc ném ngoại lệ, tùy thuộc vào cấu hình.

Khi nào cần Mock

Mock cần thiết khi mã được kiểm thử tương tác với các thành phần có tác dụng phụ: gửi dữ liệu lên máy chủ, ghi vào cơ sở dữ liệu, ghi log, analytics, điều hướng, hiển thị hộp thoại hệ thống. Không có Mock, những tương tác này không thể được xác minh mà không chạy hạ tầng thực tế. Theo Google Testing Blog, Mock là cách duy nhất để xác minh rằng một ứng dụng thực sự đã gửi sự kiện analytics mà không cần khởi động máy chủ kiểm thử.

Mock vs Stub: so sánh chi tiết

Sự khác biệt giữa MockStub là một trong những chủ đề được tranh luận nhiều nhất trong kiểm thử. Cả hai loại đều thay thế một phụ thuộc thực tế, nhưng theo những cách khác nhau về cơ bản.

Tiêu chíMockStub
Câu hỏi chínhPhương thức đã được gọi chưa?Kết quả nào được trả về?
Xác minhHành vi (verify)Trạng thái (assert)
Trả về dữ liệuTùy chọnBắt buộc
Ví dụverify(analytics).logEvent(“click”)assertEquals(5, repository.getCount())
Khi nào sử dụngTác dụng phụTrả về dữ liệu

Nguyên tắc thực tế: Mock hay không

Một bài kiểm thử đơn giản để quyết định: hãy tự hỏi “nếu tôi xóa dòng mã này, kiểm thử có thất bại không?” Nếu kiểm thử kiểm tra giá trị trả về — bạn cần Stub (xác minh dựa trên assert). Nếu kiểm thử kiểm tra mã đã gọi một phương thức với tham số chính xác — bạn cần Mock (xác minh dựa trên verify). Sự phân đôi này tuân theo mẫu Command-Query Separation: các phương thức thay đổi trạng thái (commands) cần Mock; các phương thức trả về dữ liệu (queries) cần Stub.

Mockito vs MockK: so sánh thư viện

Chọn giữa MockitoMockK là một trong những quyết định đầu tiên khi thiết lập stack kiểm thử cho dự án Android bằng Kotlin. Cả hai thư viện đều phục vụ cùng một mục đích, nhưng với các cách tiếp cận khác nhau đối với các tính năng đặc thù của Kotlin.

Mockito: cổ điển đã được kiểm chứng

Mockito là tiêu chuẩn thực tế cho các dự án Java. Phiên bản 5.x hỗ trợ mocking cho các lớp final, phương thức static và constructor nhờ MockMaker tích hợp sẵn. Đối với các dự án Kotlin, Mockito yêu cầu thiết lập bổ sung: các phần mở rộng mockito-kotlin cho cú pháp cải tiến, mockito-inline cho các lớp final. Mockito không hỗ trợ coroutine Kotlin và hàm suspend mà không có bộ chuyển đổi bổ sung.

MockK: cách tiếp cận Kotlin-đầu tiên

MockK được tạo ra đặc biệt cho Kotlin. Nó hỗ trợ gốc coroutine (coEvery, coVerify), sealed class, data class, object singleton và hàm mở rộng. Cú pháp MockK sử dụng DSL với các khối lambda, trông tự nhiên trong mã Kotlin. MockK cũng hỗ trợ mocking thuộc tính mà không cần thiết lập bổ sung — điều này quan trọng cho các dự án Android sử dụng LiveData, StateFlow và Delegates.

kotlin
// Mockito + mockito-kotlin
val repository = mock<UserRepository>()
whenever(repository.getUser(1)).thenReturn(user)

// MockK
val repository = mockk<UserRepository>()
every { repository.getUser(1) } returns user

So sánh hiệu suất

Điểm chuẩn (JVM Benchmark, 2024) cho thấy MockK tạo đối tượng mock nhanh hơn 15–20% so với Mockito cho các dự án Kotlin nhờ làm việc trực tiếp với bytecode Kotlin thay vì Java Reflections. Đối với các dự án có hàng ngàn bài kiểm thử đơn vị, sự khác biệt về tốc độ xây dựng có thể đáng chú ý: MockK tiết kiệm 30–60 giây cho một lần chạy kiểm thử đầy đủ trong các dự án lớn.

Ví dụ kiểm thử Mock trong Kotlin

Hãy xem xét ba kịch bản: kiểm thử ViewModel với các phụ thuộc Mock, kiểm thử UseCase với xác minh lệnh gọi API và kiểm thử coroutine với coVerify.

Ví dụ 1: ViewModel với Mock analytics

kotlin
class ProfileViewModelTest {
    private val analytics = mockk<AnalyticsService>()
    private val repo = mockk<UserRepository>()
    private val vm = ProfileViewModel(repo, analytics)

    fun `profile opened logs analytics event`() {
        every { analytics.logEvent("profile_opened") } returns Unit

        vm.onViewCreated()

        verify { analytics.logEvent("profile_opened") }
    }
}

Ví dụ 2: UseCase với xác minh bất đồng bộ

kotlin
class SendMessageUseCaseTest {
    private val api = mockk<MessagingApi>()
    private val useCase = SendMessageUseCase(api)

    fun `send message with correct payload`() = runTest {
        val message = Message(text = "Hello", userId = 42)

        coEvery { api.sendMessage(any()) } returns MessageResult.Sent("msg_1")

        val result = useCase.execute(message)

        coVerify {
            api.sendMessage(match {
                it.text == "Hello" && it.userId == 42
            })
        }
        assertTrue(result is MessageResult.Sent)
    }
}

Ví dụ 3: xác minh tham số với ArgumentCaptor

kotlin
class OrderUseCaseTest {
    private val api = mockk<OrderApi>()
    private val useCase = OrderUseCase(api)
    private val slot = slot<OrderRequest>()

    fun `order request contains correct items`() = runTest {
        coEvery { api.placeOrder(capture(slot)) } returns OrderResult.Placed("order_1")

        useCase.execute(listOf("item_a", "item_b"))

        assertEquals(2, slot.captured.items.size)
        assertEquals("item_a", slot.captured.items[0])
    }
}

Thực hành tốt nhất kiểm thử Mock

Sử dụng hiệu quả Mock trong phát triển di động đòi hỏi kỷ luật. Vi phạm các quy tắc này biến kiểm thử thành chướng ngại vật mong manh dễ vỡ mỗi khi tái cấu trúc.

Chỉ Mock ranh giới bên ngoài ứng dụng

Một nguyên tắc cứng: Mock chỉ nên được tạo cho các phụ thuộc vượt qua ranh giới ứng dụng: client API, cơ sở dữ liệu, hệ thống tệp, dịch vụ hệ thống (LocationManager, BluetoothAdapter, Camera). Các lớp nội bộ của ứng dụng — thực thể miền, Value Objects, tiện ích đơn giản — không nên được thay thế bằng Mock. Hành vi của chúng được kiểm thử thông qua các đối tượng thực tế.

Một assert / verify cho mỗi kiểm thử

Mỗi kiểm thử nên chứa chính xác một kiểm tra logic — verify (cho Mock) hoặc assert (cho Stub). Không trộn lẫn xác minh trạng thái và hành vi trong cùng một kiểm thử. Nếu bạn cần kiểm tra cả lệnh gọi API và kết quả của nó — hãy tạo hai kiểm thử riêng biệt với tên khác nhau. Nguyên tắc này, được gọi là “một assert cho mỗi kiểm thử”, bắt nguồn từ các khuyến nghị của Kent Beck (2002).

  • Sử dụng relaxUnitFun = true trong MockK cho các phương thức trả về Unit — nếu không Mock sẽ ném ngoại lệ khi gọi không được chỉ định
  • Giới hạn verify chỉ cho các lệnh gọi quan trọng — không xác minh mọi getter và setter, điều này làm kiểm thử mong manh
  • Áp dụng ArgumentMatchers một cách thận trọng — any() ẩn các chi tiết quan trọng nếu tham số quan trọng cho logic kinh doanh
  • Không lạm dụng verifyNoMoreInteractions — phương thức này làm cho kiểm thử cứng nhắc không cần thiết trước bất kỳ thay đổi nào trong mã sản xuất
  • Sử dụng @MockkAnnotations để khởi tạo tự động các đối tượng Mock — điều này giảm boilerplate và cải thiện khả năng đọc

Kỹ thuật kiểm thử Mock nâng cao

Ngoài mocking cơ bản, có những kỹ thuật nâng cao giải quyết các tác vụ cụ thể trong phát triển di động: kiểm thử đa luồng, xác minh trạng thái Flow và mocking một phần đối tượng thực tế.

Mock một phần với spyK

Spy (hoặc mock một phần) cho phép tạo một đối tượng ủy quyền các lệnh gọi cho triển khai thực tế nhưng cho phép ghi đè các phương thức riêng lẻ. Trong MockK, spyk được tạo dựa trên một thực thể lớp thực tế: val repo = spyk(InMemoryUserRepository()). Các lệnh gọi có kỳ vọng được xác định qua every đi qua Mock; phần còn lại đi qua đối tượng thực tế. Spy đặc biệt hữu ích để kiểm thử mã kế thừa nơi tiêm phụ thuộc chưa được triển khai và bạn chỉ cần ghi đè một phương thức.

Kiểm thử StateFlow với Turbine

Trong các dự án Android hiện đại sử dụng Jetpack Compose, ViewModel hiển thị trạng thái qua StateFlow. MockK cho phép mocking các phụ thuộc Flow, và thư viện Turbine đơn giản hóa việc xác minh phát xạ. Mẫu cổ điển: MockK cho UseCase trả về Flow, Turbine để xác minh các phát xạ của ViewModel. Stack này được khuyến nghị bởi tài liệu Android Testing (Google, 2024) cho các dự án sử dụng Kotlin Coroutines.

kotlin
class SearchViewModelTest {
    private val searchUseCase = mockk<SearchUseCase>()
    private val vm = SearchViewModel(searchUseCase)

    fun `search emits results`() = runTest {
        coEvery { searchUseCase.search("android") } returns
            flowOf(SearchResult.Success(listOf(Item("Android TDD"))))

        vm.search("android")

        vm.state.test {
            val state = awaitItem()
            assertTrue(state.items.isNotEmpty())
            cancelAndIgnoreRemainingEvents()
        }
    }
}

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

Mock khác Mockito như thế nào?

Mock là một khái niệm, một loại Test Double kiểm tra hành vi. Mockito là thư viện tạo đối tượng Mock trong Java và Android. Các thư viện khác: MockK (Kotlin), EasyMock (Java), Cuckoo (iOS).

Mock hoạt động với coroutine Kotlin như thế nào?

Để kiểm thử hàm suspend với Mock, sử dụng MockK (coEvery / coVerify) hoặc Mockito với mockito-kotlin. MockK hỗ trợ coroutine gốc: coEvery xác định hành vi của hàm suspend, coVerify xác minh lệnh gọi của nó bên trong một coroutine. Tất cả các lệnh gọi suspend phải được thực thi bên trong runTest (kotlinx-coroutines-test).

Mock có thể trả về các giá trị khác nhau khi gọi lặp lại không?

Có. Trong MockK, sử dụng returnsMany: every { api.getData() } returnsMany listOf(response1, response2). Trong Mockito — một chuỗi thenReturn(value1).thenReturn(value2). Điều này hữu ích để kiểm thử hành vi với các lệnh gọi tuần tự trả về các phản hồi khác nhau.

Làm thế nào để xóa trạng thái Mock giữa các kiểm thử?

Trong MockK, sử dụng chú thích @MockK với relaxed = true và gọi clearMocks(mock) trong phương thức @After. Trong MockitoMockito.reset(mock). Thực hành tốt nhất: tạo Mock mới cho mỗi kiểm thử qua @Before để loại bỏ sự can thiệp giữa các kiểm thử.

Mock xử lý sealed class trong Kotlin như thế nào?

MockK hoạt động chính xác với sealed class: every { useCase() } returns Result.Success(data). Mockito không hỗ trợ sealed class trực tiếp, yêu cầu các giải pháp thay thế. Đây là một lý do tại sao MockK được khuyến nghị cho các dự án Kotlin thay vì Mockito.

Tóm tắt

  • Mock — một loại Test Double xác minh hành vi (verify), không phải trạng thái (assert) của các phụ thuộc
  • Mockito — tiêu chuẩn cho Java/Android, MockK — lựa chọn Kotlin-đầu tiên với hỗ trợ coroutine và sealed class
  • Nguyên tắc chính: Mock cho ranh giới bên ngoài (mạng, DB, dịch vụ hệ thống), đối tượng thực cho lớp nội bộ
  • Over-mocking — phản mẫu chính: thay thế phụ thuộc quá mức làm kiểm thử mong manh và vô ích
  • Một kiểm thử — một kiểm tra logic: verify cho Mock hoặc assert cho Stub, không cả hai trong cùng một kiểm thử
  • ArgumentCaptor / slot — cách chính xác để xác minh tham số gọi Mock thay vì any() mù quáng
  • MockK được khuyến nghị cho các dự án Kotlin: coEverycoVerify hoạt động gốc với coroutine không cần bộ chuyển đổi bổ sung

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