Mock คือออบเจกต์ทดแทนที่เลียนแบบพฤติกรรมของคอมโพเนนต์จริง และช่วยให้ตรวจสอบการโต้ตอบกับมันได้ ต่างจาก Stub ที่เพียงคืนค่าที่กำหนดไว้ล่วงหน้า Mock จะบันทึกข้อเท็จจริงของการเรียกเมธอด อาร์กิวเมนต์ที่ส่งผ่าน และจำนวนการเรียก ตามข้อมูลจาก Mockito (2024) Mock เป็นประเภท Test Double ที่ได้รับความนิยมมากที่สุดในโปรเจกต์ Java และ Kotlin ใช้ในการทดสอบหน่วยมากกว่า 70% ของแอปพลิเคชันมือถือ
ประเด็นสำคัญ
Mock คือออบเจกต์ที่สร้างโดยเฟรมเวิร์ก mocking (Mockito, MockK, EasyMock) ซึ่งจำลองอินเทอร์เฟซหรือคลาส และบันทึกการเรียกเมธอดทั้งหมด นักพัฒนากำหนดความคาดหวัง: เมธอด X จะถูกเรียกด้วยอาร์กิวเมนต์ Y และจะคืนค่า Z หลังจากการทดสอบดำเนินการ Mock จะตรวจสอบว่าความคาดหวังตรงกับการเรียกจริงหรือไม่
คำนี้มาจาก อุปลักษณ์การแสดงละคร ของ Test Doubles: Mock คือ “ผู้เลียนแบบ” ที่ไม่เพียงยืนบนเวที (เหมือน Dummy) แต่แสดงบทบาทและตรวจสอบว่าการโต้ตอบกับมันถูกต้องหรือไม่ หากโค้ดที่ทดสอบไม่ได้เรียกเมธอดที่ Mock คาดหวัง หรือเรียกด้วยอาร์กิวเมนต์ที่ไม่ถูกต้อง — การทดสอบล้มเหลวพร้อมข้อความความคาดหวังที่ถูกละเมิด
Mock ถูกสร้างขึ้นผ่านโรงงานของเฟรมเวิร์ก: mockk<MyInterface>() หรือ Mockito.mock(MyClass.java) เฟรมเวิร์กสร้างออบเจกต์พร็อกซีที่สกัดกั้นการเรียกเมธอดทั้งหมด แต่ละการเรียกจะถูกเปรียบเทียบกับความคาดหวังที่กำหนดไว้ล่วงหน้า หากการเรียกตรงกับความคาดหวัง — ค่าที่ระบุจะถูกคืนค่า หากไม่ — Mock จะคืนค่าเริ่มต้นหรือโยนข้อยกเว้น ขึ้นอยู่กับการกำหนดค่า
Mock จำเป็นเมื่อโค้ดที่ทดสอบโต้ตอบกับคอมโพเนนต์ที่มีผลข้างเคียง: การส่งข้อมูลไปยังเซิร์ฟเวอร์ การเขียนลงฐานข้อมูล การบันทึก log analytics การนำทาง การแสดงไดอะล็อกของระบบ หากไม่มี Mock การโต้ตอบเหล่านี้ไม่สามารถตรวจสอบได้โดยไม่ต้องเรียกใช้โครงสร้างพื้นฐานจริง ตาม Google Testing Blog Mock เป็นวิธีเดียวที่จะตรวจสอบว่าแอปพลิเคชันส่งอีเวนต์ analytics จริงหรือไม่โดยไม่ต้องเปิดเซิร์ฟเวอร์ทดสอบ
ความแตกต่างระหว่าง Mock และ Stub เป็นหนึ่งในหัวข้อที่มีการถกเถียงมากที่สุดในการทดสอบ ทั้งสองประเภทแทนที่การพึ่งพาจริง แต่ด้วยวิธีที่แตกต่างกันโดยพื้นฐาน
| เกณฑ์ | Mock | Stub |
|---|---|---|
| คำถามหลัก | เมธอดถูกเรียกหรือไม่? | ผลลัพธ์ใดถูกคืนค่า? |
| การตรวจสอบ | พฤติกรรม (verify) | สถานะ (assert) |
| การคืนข้อมูล | ไม่บังคับ | บังคับ |
| ตัวอย่าง | verify(analytics).logEvent(“click”) | assertEquals(5, repository.getCount()) |
| เมื่อใดควรใช้ | ผลข้างเคียง | การคืนข้อมูล |
การทดสอบง่ายๆ เพื่อตัดสินใจ: ถามตัวเองว่า “ถ้าฉันลบบรรทัดโค้ดนี้ การทดสอบจะล้มเหลวหรือไม่?” หากการทดสอบตรวจสอบค่าที่คืน — คุณจำเป็นต้องใช้ Stub (การตรวจสอบแบบ assert) หากการทดสอบตรวจสอบว่าโค้ดเรียกเมธอดด้วยอาร์กิวเมนต์ที่ถูกต้อง — คุณจำเป็นต้องใช้ Mock (การตรวจสอบแบบ verify) การแบ่งขั้วนี้เป็นไปตามแพทเทิร์น Command-Query Separation: เมธอดที่เปลี่ยนสถานะ (commands) จำเป็นต้องใช้ Mock; เมธอดที่คืนข้อมูล (queries) จำเป็นต้องใช้ Stub
การเลือกระหว่าง Mockito และ MockK เป็นหนึ่งในการตัดสินใจแรกเมื่อตั้งค่าสแต็กการทดสอบสำหรับโปรเจกต์ Android ใน Kotlin ไลบรารีทั้งสองทำหน้าที่เดียวกัน แต่มีแนวทางที่แตกต่างกันต่อคุณสมบัติเฉพาะของ Kotlin
Mockito เป็นมาตรฐานโดยพฤตินัยสำหรับโปรเจกต์ Java เวอร์ชัน 5.x รองรับการ mock สำหรับคลาส final เมธอด static และคอนสตรัคเตอร์ ต้องขอบคุณ MockMaker ในตัว สำหรับโปรเจกต์ Kotlin Mockito ต้องการการตั้งค่าเพิ่มเติม: ส่วนขยาย mockito-kotlin สำหรับไวยากรณ์ที่ดีขึ้น mockito-inline สำหรับคลาส final Mockito ไม่รองรับ coroutine ของ Kotlin และฟังก์ชัน suspend โดยไม่มีอะแดปเตอร์เพิ่มเติม
MockK ถูกสร้างขึ้นสำหรับ Kotlin โดยเฉพาะ รองรับ coroutine (coEvery, coVerify) sealed class data class object singleton และฟังก์ชัน extension โดยกำเนิด ไวยากรณ์ของ MockK ใช้ DSL กับบล็อกแลมบ์ดา ซึ่งดูเป็นธรรมชาติในโค้ด Kotlin MockK ยังรองรับการ mock คุณสมบัติ (property mocking) โดยไม่ต้องตั้งค่าเพิ่มเติม — ซึ่งสำคัญสำหรับโปรเจกต์ Android ที่ใช้ LiveData StateFlow และ Delegates
// Mockito + mockito-kotlin
val repository = mock<UserRepository>()
whenever(repository.getUser(1)).thenReturn(user)
// MockK
val repository = mockk<UserRepository>()
every { repository.getUser(1) } returns user
การวัดประสิทธิภาพ (JVM Benchmark, 2024) แสดงให้เห็นว่า MockK สร้างออบเจกต์ mock เร็วกว่า Mockito 15–20% สำหรับโปรเจกต์ Kotlin เนื่องจากทำงานโดยตรงกับไบต์โค้ดของ Kotlin แทนที่จะเป็น Java Reflections สำหรับโปรเจกต์ที่มีการทดสอบหน่วยเป็นพันๆ ความแตกต่างของความเร็วในการ build อาจเห็นได้ชัด: MockK ประหยัดเวลา 30–60 วินาทีในการรันทดสอบทั้งหมดในโปรเจกต์ขนาดใหญ่
มาดูสามสถานการณ์: การทดสอบ ViewModel ด้วยการพึ่งพา Mock การทดสอบ UseCase ด้วยการตรวจสอบการเรียก API และการทดสอบ coroutine ด้วย coVerify
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") }
}
}
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)
}
}
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])
}
}
การใช้ Mock อย่างมีประสิทธิภาพในการพัฒนาแอปมือถือต้องมีวินัย การละเมิดกฎเหล่านี้จะเปลี่ยนการทดสอบให้เป็นอุปสรรคที่เปราะบางซึ่งพังทุกครั้งที่มีการปรับโครงสร้างโค้ด
กฎที่เคร่งครัด: ควรสร้าง Mock สำหรับการพึ่งพาที่ข้ามขอบเขตของแอปพลิเคชันเท่านั้น: ไคลเอนต์ API ฐานข้อมูล ระบบไฟล์ บริการระบบ (LocationManager, BluetoothAdapter, Camera) คลาสภายในของแอปพลิเคชัน — เอนทิตีโดเมน Value Objects ยูทิลิตี้ธรรมดา — ไม่ควรถูกแทนที่ด้วย Mock พฤติกรรมของพวกเขาจะถูกทดสอบผ่านออบเจกต์จริง
แต่ละการทดสอบควรมีการตรวจสอบเชิงตรรกะเพียงหนึ่งเดียว — verify (สำหรับ Mock) หรือ assert (สำหรับ Stub) อย่าผสมการตรวจสอบสถานะและพฤติกรรมในการทดสอบเดียว หากคุณต้องการตรวจสอบทั้งการเรียก API และผลลัพธ์ — ให้สร้างการทดสอบแยกกันสองรายการด้วยชื่อที่แตกต่างกัน กฎนี้เรียกว่า “หนึ่ง assert ต่อการทดสอบ” ย้อนกลับไปถึงคำแนะนำของ Kent Beck (2002)
นอกเหนือจากการ mock พื้นฐาน ยังมีเทคนิคขั้นสูงที่แก้ไขงานเฉพาะในการพัฒนาแอปมือถือ: การทดสอบมัลติเธรด การตรวจสอบสถานะ Flow และการ mock บางส่วนของออบเจกต์จริง
Spy (หรือ mock บางส่วน) ช่วยให้สร้างออบเจกต์ที่มอบหมายการเรียกไปยังการใช้งานจริง แต่ให้แทนที่เมธอดแต่ละรายการได้ ใน MockK spyk ถูกสร้างขึ้นจากอินสแตนซ์คลาสจริง: val repo = spyk(InMemoryUserRepository()) การเรียกที่มีความคาดหวังที่กำหนดผ่าน every จะผ่าน Mock; ส่วนที่เหลือจะผ่านออบเจกต์จริง Spy มีประโยชน์โดยเฉพาะสำหรับการทดสอบโค้ดเก่าที่ยังไม่ได้ใช้การฉีดการพึ่งพา และคุณต้องการแทนที่เพียงเมธอดเดียว
ในโปรเจกต์ Android สมัยใหม่ที่ใช้ Jetpack Compose ViewModel เปิดเผยสถานะผ่าน StateFlow MockK อนุญาตให้ mock การพึ่งพา Flow และไลบรารี Turbine ทำให้การตรวจสอบการปล่อยค่าทำได้ง่ายขึ้น รูปแบบคลาสสิก: MockK สำหรับ UseCase ที่คืนค่า Flow, Turbine สำหรับตรวจสอบการปล่อยค่าของ ViewModel สแต็กนี้ได้รับการแนะนำโดยเอกสาร Android Testing (Google, 2024) สำหรับโปรเจกต์ที่ใช้ Kotlin Coroutines
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()
}
}
}
คำถามที่พบบ่อย
Mock เป็นแนวคิด ประเภทของ Test Double ที่ตรวจสอบพฤติกรรม Mockito เป็นไลบรารีสำหรับสร้างออบเจกต์ Mock ใน Java และ Android ไลบรารีอื่นๆ: MockK (Kotlin), EasyMock (Java), Cuckoo (iOS)
สำหรับการทดสอบฟังก์ชัน suspend ด้วย Mock ให้ใช้ MockK (coEvery / coVerify) หรือ Mockito กับ mockito-kotlin MockK รองรับ coroutine โดยกำเนิด: coEvery กำหนดพฤติกรรมของฟังก์ชัน suspend, coVerify ตรวจสอบการเรียกภายใน coroutine การเรียก suspend ทั้งหมดต้องดำเนินการภายใน runTest (kotlinx-coroutines-test)
ได้ ใน MockK ใช้ returnsMany: every { api.getData() } returnsMany listOf(response1, response2) ใน Mockito — สายโซ่ของ thenReturn(value1).thenReturn(value2) ซึ่งมีประโยชน์สำหรับการทดสอบพฤติกรรมที่มีการเรียกตามลำดับซึ่งคืนค่าที่แตกต่างกัน
ใน MockK ใช้คำอธิบายประกอบ @MockK ด้วย relaxed = true และเรียก clearMocks(mock) ในเมธอด @After ใน Mockito — Mockito.reset(mock) แนวทางปฏิบัติที่ดีที่สุด: สร้าง Mock ใหม่สำหรับแต่ละการทดสอบผ่าน @Before เพื่อกำจัดการแทรกแซงระหว่างการทดสอบ
MockK ทำงานได้อย่างถูกต้องกับ sealed class: every { useCase() } returns Result.Success(data) Mockito ไม่รองรับ sealed class โดยตรง ต้องใช้วิธีแก้ไข นี่คือเหตุผลหนึ่งที่แนะนำ MockK สำหรับโปรเจกต์ Kotlin แทน Mockito
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม