MockK คือเฟรมเวิร์กแบบ Kotlin-first สำหรับสร้างออบเจกต์จำลอง (mock objects) ที่ออกแบบมาโดยเฉพาะสำหรับระบบนิเวศของ Kotlin โดยคำนึงถึงคุณลักษณะทางภาษา: โครูทีน, ฟังก์ชันส่วนขยาย, data class และ sealed class ต่างจาก Mockito ที่ถูกพอร์ตจาก Java ไปยัง Kotlin MockK ถูกออกแบบมาสำหรับไวยากรณ์ของ Kotlin ตั้งแต่แรก และไม่จำเป็นต้องมีปลั๊กอินเพิ่มเติมเพื่อทำงานกับคลาส final ตามข้อมูลจาก MockK.io ไลบรารีนี้ถูกใช้ในโปรเจกต์ Kotlin ที่มีการทดสอบหน่วยมากกว่า 40%
ประเด็นสำคัญ
MockK คือไลบรารีสำหรับสร้างออบเจกต์จำลอง เขียนด้วย Kotlin และปรับให้เหมาะสมกับไวยากรณ์ของมัน มันแก้ปัญหาเดียวกันกับที่ Mockito แก้ — การแยกโค้ดที่ทดสอบออกจากการพึ่งพา — แต่ทำโดยใช้โครงสร้างเฉพาะของ Kotlin: แลมบ์ดา, DSL, reified generics และฟังก์ชัน suspend
ข้อได้เปรียบหลักของ MockK เหนือโซลูชันที่ถูกพอร์ตมาคือ การรองรับ Kotlin โดยกำเนิด ใน Mockito การจำลองคลาส final ต้องใช้ opt-in (mockito-inline) และเมธอดสแตติกต้องใช้ mockStatic MockK รองรับสิ่งนี้โดยดีฟอลต์ เนื่องจากคลาส Kotlin เป็น final โดยดีฟอลต์ และการเลี่ยงข้อจำกัดนี้ถูกสร้างไว้ในสถาปัตยกรรมของไลบรารี
เวอร์ชัน 1.13.12 (2024) เป็นรุ่นเสถียรที่รองรับ Kotlin 2.0, คอมไพเลอร์ K2 และโปรเจกต์ข้ามแพลตฟอร์ม (KMP) MockK ยังทำงานกับ Kotlin/Native และ Kotlin/JS ทำให้เป็นตัวเลือกเดียวสำหรับโปรเจกต์ KMP ที่ทั้ง Mockito และ EasyMock ไม่สามารถใช้งานได้
MockK ถูกออกแบบโดยคำนึงถึงลักษณะเฉพาะของ Kotlin และใช้คุณสมบัติภาษา — reified generics, DSL กับแลมบ์ดา, ฟังก์ชัน inline — เพื่อให้ API ที่กระชับและ type-safe โดยไม่เสียสละประสิทธิภาพ
กลไกของ MockK อาศัยการตรวจสอบไบต์โค้ดผ่านไลบรารี ByteBuddy (เช่นเดียวกับ Mockito) แต่ห่อหุ้มไว้ใน DSL ที่เป็นมิตรกับ Kotlin แทนที่จะใช้ลูกโซ่ when().thenReturn() MockK ใช้บล็อกแลมบ์ดา every { } และ coEvery { } ที่ดูเหมือนส่วนขยายตามธรรมชาติของภาษา ภายใต้ฝาครอบ MockK จะสกัดกั้นการเรียกภายในแลมบ์ดา วิเคราะห์เมธอดและอาร์กิวเมนต์ผ่านรีเฟลกชัน และจับคู่กับกฎ stubbing ที่บันทึกไว้
บล็อก every { mock.method() } returns value อ่านว่า “ทุกครั้งที่เรียกเมธอด ให้ส่งค่ากลับ” ไวยากรณ์แบบประกาศนี้ใกล้เคียงกับสไตล์ Kotlin มากกว่าและขจัดความสับสนเกี่ยวกับลำดับอาร์กิวเมนต์ใน when() ต้องขอบคุณ reified generics ของ Kotlin ทำให้ชนิดของ mock ถูกอนุมานโดยอัตโนมัติโดยไม่ต้องระบุคลาสอย่างชัดเจน
val repository = mockk<UserRepository>()
// Stubbing: การเรียก findById(1) แต่ละครั้งส่งผู้ใช้กลับ
every { repository.findById(1) } returns User("Alice")
// การเรียกและการตรวจสอบ
val result = repository.findById(1)
assertEquals("Alice", result.name)
ต่างจาก Mockito ที่แต่ละเมธอดต้องกำหนดค่าอย่างชัดเจน MockK รองรับ relaxed mock — mock ที่ส่งค่าดีฟอลต์ที่ “สมเหตุสมผล” สำหรับเมธอดใดๆ: รายการว่างสำหรับ List, 0 สำหรับ Int, สตริงว่างสำหรับ String ซึ่งช่วยลดปริมาณโค้ดตั้งค่าได้อย่างมาก
// Relaxed mock — เมธอดทั้งหมดส่งค่าดีฟอลต์
val api = mockk<ApiService>(relaxed = true)
// ไม่ต้อง stubbing — ส่งรายการว่างกลับ
println(api.getUsers()) // []
MockK มีหลายวิธีในการสร้างออบเจกต์จำลอง: mockk<T>() สำหรับ mock แบบเข้มงวด (แต่ละเมธอดต้องกำหนดค่าอย่างชัดเจน), mockk<T>(relaxed = true) สำหรับ mock แบบผ่อนคลาย และ spyk(obj) สำหรับสร้าง spy บนออบเจกต์จริง
| ฟังก์ชัน | ชนิด | พฤติกรรมโดยไม่มี stubbing |
|---|---|---|
| mockk() | Mock แบบเข้มงวด | โยนข้อยกเว้นเมื่อเรียกเมธอดที่ไม่ได้ stub |
| mockk(relaxed = true) | Mock แบบผ่อนคลาย | ส่งค่าดีฟอลต์ |
| spyk() | Spy | เรียกเมธอดจริงถ้าไม่ได้กำหนด stub |
| slot() | Argument Captor | จับอาร์กิวเมนต์สำหรับการตรวจสอบ |
การเลือกระหว่าง mock แบบเข้มงวดและผ่อนคลายขึ้นอยู่กับบริบท Mock แบบเข้มงวดรับประกันว่าการทดสอบจะไม่ใช้เมธอดที่ไม่ได้กำหนดพฤติกรรม — ซึ่งเพิ่มความน่าเชื่อถือ Mock แบบผ่อนคลายสะดวกสำหรับการสร้างต้นแบบการทดสอบอย่างรวดเร็วเมื่อการพึ่งพาทั้งหมดไม่สำคัญ ในทางปฏิบัติ แนะนำให้เริ่มต้นด้วย mock แบบเข้มงวดและเปลี่ยนเป็นแบบผ่อนคลายเฉพาะเมื่อ stubbing ใช้บรรทัดมากกว่าการทดสอบเอง
บล็อก every คือโครงสร้างหลักของ stubbing ใน MockK ภายในแลมบ์ดา มีการอธิบายการเรียกเมธอดด้วยอาร์กิวเมนต์เฉพาะ จากนั้นส่งค่ากลับผ่าน returns, โยนข้อยกเว้นผ่าน throws หรือคำนวณคำตอบผ่าน answers
MockK รองรับสถานการณ์ทั้งหมดที่จำเป็นสำหรับการทดสอบ: การส่งค่ากลับ, การโยนข้อยกเว้น, การคำนวณคำตอบตามอาร์กิวเมนต์, และคำตอบหลายรายการตามลำดับ (ลำดับการเรียก)
// ส่งค่ากลับ
every { repo.findById(1) } returns User("Alice")
// โยนข้อยกเว้น
every { repo.findById(999) } throws NotFoundException()
// คำตอบแบบไดนามิก
every { repo.save(any()) } answers {
val user = firstArg<User>()
user.copy(id = 42)
}
// ลำดับคำตอบ
every { repo.findAll() } returnsMany listOf(
listOf(User("Alice")),
listOf(User("Bob")),
emptyList()
)
Verify ใน MockK มีวัตถุประสงค์คล้ายกับ Mockito.verify() แต่ใช้ DSL ของ Kotlin: verify { mock.method() } สำหรับฟังก์ชัน suspend ใช้ coVerify { mock.suspendMethod() } ซึ่งทำงานถูกต้องกับโครูทีนและไม่ต้องการรันเนอร์พิเศษ
MockK รองรับตัวปรับแต่งเดียวกันกับ Mockito: exactly(1), atLeast(2), atMost(5), wasNot(Called) ไวยากรณ์เรียบง่าย — ตัวปรับแต่งถูกส่งเป็นอาร์กิวเมนต์แรกใน verify { }
// การตรวจสอบ: เมธอดถูกเรียก 1 ครั้งพอดี
verify(exactly = 1) { repo.save(any()) }
// ตรวจสอบลำดับการเรียก
verifySequence {
repo.save(any())
repo.flush()
}
// coVerify สำหรับฟังก์ชัน suspend
coVerify { api.fetchUsers() }
สำหรับการตรวจสอบอาร์กิวเมนต์ ใช้ slot() — ซึ่งคล้ายกับ ArgumentCaptor Slot ถูกประกาศก่อนการเรียก ส่งต่อไปยัง every หรือ verify และหลังจากการทดสอบทำงาน จะมีค่าที่ถูกจับไว้
val userSlot = slot<User>()
verify { repo.save(capture(userSlot)) }
assertEquals("Alice", userSlot.captured.name)
MockK มีคำอธิบายประกอบ @MockK และ @RelaxedMockK สำหรับสร้าง mock ผ่านการเริ่มต้นใน JUnit 5 ส่วนขยาย MockKExtension จะสร้าง mock โดยอัตโนมัติก่อนการทดสอบแต่ละครั้งและทำความสะอา�ะหลัง — คล้ายกับ MockitoExtension แต่รองรับโหมดผ่อนคลาย
คำอธิบายประกอบ @InjectMockKs (หรือทางเลือก @MockK พร้อมการสร้างออบเจกต์อย่างชัดแจ้ง) ฉีด mock เข้าไปในอินสแตนซ์ที่ทดสอบ ซึ่งช่วยลดโบยเลอร์เพลทและทำให้โค้ดทดสอบสะอาดขึ้น
@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)
}
}
การเลือกระหว่าง MockK และ Mockito ขึ้นอยู่กับองค์ประกอบของทีมและชนิดของโปรเจกต์ Mockito มีระบบนิเวศที่ใหญ่กว่า ตัวอย่างและการรวมระบบมากกว่า แต่ MockK ให้ไวยากรณ์ Kotlin ที่สะอาดกว่าและการรองรับคุณสมบัติภาษาโดยกำเนิด สำหรับโปรเจกต์ Kotlin ใหม่ MockK ถูกแนะนำให้เป็นโซลูชันที่เป็นธรรมชาติมากกว่า
| เกณฑ์ | MockK | Mockito |
|---|---|---|
| ไวยากรณ์ | Kotlin DSL (every, verify) | สไตล์ Java (when, thenReturn) |
| โครูทีน | coEvery, coVerify (โดยกำเนิด) | ต้องใช้ไลบรารีเพิ่มเติม |
| คลาส final | รองรับโดยดีฟอลต์ | ต้องใช้ mockito-inline |
| KMP | รองรับ | ไม่รองรับ |
| Relaxed mock | ในตัว | ไม่มีสิ่งที่เทียบเท่า |
| ความนิยม | เพิ่มขึ้นในชุมชน Kotlin | โดดเด่นในโปรเจกต์ Java และไฮบริด |
สำหรับโปรเจกต์ Kotlin บริสุทธิ์ (ไม่มีคลาส Java) MockK เหมาะกว่า: โบยเลอร์เพลทน้อยกว่า, รองรับโครูทีนโดยกำเนิด, ไม่มีเซอร์ไพรส์กับคลาส final สำหรับโปรเจกต์ไฮบริดหรือทีมที่มีพื้นฐาน Java MockKit ยังคงเป็นตัวเลือกที่ใช้งานได้ — ไลบรารีทั้งสองสามารถใช้ในโปรเจกต์เดียวกันผ่านโมดูลต่างๆ เมื่อย้ายจาก Mockito ไปยัง MockK ก็เพียงพอที่จะแทนที่คำอธิบายประกอบ @Mock ด้วย @MockK และเขียนบล็อก when().thenReturn() ใหม่ในรูปแบบ every { }
คำถามที่พบบ่อย
Relaxed mock ส่งค่าดีฟอลต์สำหรับเมธอดที่ไม่ได้ stub ทั้งหมด (รายการว่าง, 0, null) โดยไม่โยนข้อยกเว้น Mock ปกติ (แบบเข้มงวด) ต้องการ stubbing อย่างชัดแจ้งของแต่ละเมธอด — มิฉะนั้นการทดสอบจะล้มเหลว Relaxed mock สะดวกสำหรับการทดสอบรวดเร็ว แบบเข้มงวดสำหรับการทดสอบที่เชื่อถือได้
MockK รองรับการจำลองฟังก์ชันส่วนขยายผ่าน mockkStatic() สิ่งนี้เป็นไปได้เพราะฟังก์ชันส่วนขยายใน Kotlin เป็นเมธอดสแตติกที่มีตัวรับเป็นพารามิเตอร์แรก สำหรับฟังก์ชันส่วนขยายแต่ละตัว คุณต้องระบุคลาสที่ประกาศไว้
ใช่ MockK รองรับ Kotlin Multiplatform (KMP) สำหรับโค้ดทั่วไป บนแพลตฟอร์ม JVM, Native และ JS คุณสามารถใช้ API ทั่วไป: mockk(), every, verify สิ่งนี้ทำให้ MockK เป็นตัวเลือกเดียวสำหรับโปรเจกต์ KMP ที่ Mockito ไม่ทำงาน
ใช้ verifySequence { } — บล็อกที่ระบุการเรียกตามลำดับที่คาดหวังอย่างเคร่งครัด หากลำดับจริงต่างออกไป verifySequence จะโยนข้อยกเว้นระบุการเรียกที่ไม่ตรงกันครั้งแรก
ใช่ ในทางเทคนิค เป็นไปได้ แต่ไม่แนะนำ ความขัดแย้งอาจเกิดขึ้นที่ระดับการตรวจสอบไบต์โค้ด (ByteBuddy vs mockito-inline) หากโปรเจกต์ใช้ MockKit อยู่แล้ว การย้ายไปยัง MockK สามารถทำได้ทีละน้อยผ่านการแยกโมดูล
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม