MockK هو إطار عمل Kotlin-first لإنشاء الكائنات الوهمية (mock objects)، صمم خصيصاً للنظام البيئي لـ Kotlin مع مراعاة ميزات لغته: الكوروتينات، دوال التمديد، data class و sealed class. على عكس Mockito الذي تم نقله إلى Kotlin من Java، صمم MockK أصلاً لبناء جملة Kotlin ولا يتطلب إضافات إضافية للعمل مع الفئات النهائية (final classes). وفقاً لـ MockK.io، تستخدم المكتبة في أكثر من 40% من مشاريع Kotlin التي تحتوي على اختبارات وحدات.
الرئيسي
MockK هي مكتبة لإنشاء الكائنات الوهمية، مكتوبة بلغة Kotlin ومحسنة لبناء جملتها. تحل نفس المشكلات التي يحلها Mockito — عزل الكود المختبر عن التبعيات — لكنها تفعل ذلك باستخدام إنشاءات خاصة بلغة Kotlin: lambdas، DSL، reified generics ودوال suspend.
الميزة الرئيسية لـ MockK مقارنة بالحلول المنقولة هي الدعم الأصلي لـ Kotlin. في Mockito، يتطلب إنشاء كائن وهمي لفئة نهائية opt-in (mockito-inline)، والطرق الثابتة تتطلب mockStatic. MockK يدعم هذا افتراضياً، لأن فئات Kotlin تكون نهائية افتراضياً، وتجاوز هذا القيد مدمج في بنية المكتبة.
الإصدار 1.13.12 (2024) هو إصدار مستقر يدعم Kotlin 2.0 ومترجم K2 والمشاريع متعددة المنصات (KMP). يعمل MockK أيضاً مع Kotlin/Native و Kotlin/JS، مما يجعله الخيار الوحيد لمشاريع KMP حيث لا يمكن استخدام Mockito ولا EasyMock.
صمم MockK مع مراعاة خصوصيات Kotlin ويستخدم ميزات اللغة — reified generics، DSL مع lambdas، الدوال المضمنة — لتوفير API موجز وآمن من حيث النوع دون التضحية بالأداء.
آلية MockK تستند إلى أداة bytecode instrumentation عبر مكتبة ByteBuddy (مثل Mockito)، لكنها تغلفها في DSL مناسب لـ Kotlin. بدلاً من سلاسل when().thenReturn()، يستخدم MockK كتل lambda every { } و coEvery { } التي تبدو كامتداد طبيعي للغة. تحت الغطاء، يعترض MockK الاستدعاء داخل lambda، ويحلل الطريقة والوسائط من خلال الانعكاس (reflection) ويطابقها مع قواعد stubbing المسجلة.
تقرأ الكتلة every { mock.method() } returns value كـ «في كل مرة يتم استدعاء الطريقة، أعد القيمة.» هذا البناء التصريحي أقرب إلى أسلوب Kotlin ويزيل الارتباك حول ترتيب الوسائط في when(). بفضل reified generics في Kotlin، يتم استنتاج نوع الكائن الوهمي تلقائياً دون تحديد الفئة بشكل صريح.
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 — كائن وهمي يعيد قيماً افتراضية «معقولة» لأي طريقة: قائمة فارغة لـ List، 0 لـ Int، سلسلة فارغة لـ String. هذا يقلل بشكل كبير من كمية كود الإعداد.
// Relaxed mock — جميع الطرق تعيد قيماً افتراضية
val api = mockk<ApiService>(relaxed = true)
// لا يتطلب stubbing — يعيد قائمة فارغة
println(api.getUsers()) // []
MockK يقدم عدة طرق لإنشاء الكائنات الوهمية: mockk<T>() للكائنات الوهمية الصارمة (يجب تكوين كل طريقة بشكل صريح)، mockk<T>(relaxed = true) للكائنات الوهمية المرنة، و spyk(obj) لإنشاء متجسس على كائن حقيقي.
| الدالة | النوع | السلوك بدون stubbing |
|---|---|---|
| mockk() | كائن وهمي صارم | يرمي استثناء عند استدعاء طريقة غير مهيأة |
| mockk(relaxed = true) | كائن وهمي مرن | يعيد القيمة الافتراضية |
| spyk() | متجسس | يستدعي الطريقة الحقيقية إذا لم يتم تكوين stub |
| slot() | التقاط الوسيطات | يلتقط الوسيطة للتحقق |
الاختيار بين الكائن الوهمي الصارم والمرن يعتمد على السياق. الكائن الوهمي الصارم يضمن أن الاختبار لا يستخدم طرقاً سلوكها غير محدد — هذا يزيد من الموثوقية. الكائن الوهمي المرن مناسب للنمذجة السريعة للاختبارات حيث ليست كل التبعيات مهمة. عملياً، يُنصح بالبدء بكائن وهمي صارم والتحول إلى المرن فقط عندما يستغرق stubbing سطوراً أكثر من الاختبار نفسه.
كتلة every هي البناء المركزي لـ stubbing في MockK. داخل lambda، يتم وصف استدعاء طريقة مع وسائط محددة، ثم يتم إرجاع قيمة عبر 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 { }.
// التحقق: الطريقة استدعيت مرة واحدة بالضبط
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 لإنشاء الكائنات الوهمية من خلال التهيئة في JUnit 5. الملحق MockKExtension ينشئ تلقائياً الكائنات الوهمية قبل كل اختبار وينظف بعدها — مشابه لـ MockitoExtension، لكن مع دعم الوضع المرن.
التعليق @InjectMockKs (أو البديل @MockK مع إنشاء كائن صريح) يحقن الكائنات الوهمية في المثيل المختبر. هذا يقلل من الكود المتكرر ويجعل كود الاختبار أنظف.
@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 |
|---|---|---|
| بناء الجملة | DSL خاص بـ Kotlin (every, verify) | أسلوب Java (when, thenReturn) |
| الكوروتينات | coEvery, coVerify (أصلي) | يتطلب مكتبات إضافية |
| الفئة النهائية | مدعوم افتراضياً | يتطلب mockito-inline |
| KMP | مدعوم | غير مدعوم |
| Relaxed mock | مدمج | لا يوجد مقابل |
| الشعبية | متزايدة في مجتمع Kotlin | تهيمن في مشاريع Java والهجينة |
للمشاريع على Kotlin النقي (بدون فئات Java)، MockK مفضل: كود أقل، دعم أصلي للكوروتينات، لا مفاجآت مع الفئات النهائية. للمشاريع الهجينة أو الفرق ذات الخلفية في Java، يبقى Mockito خياراً عملياً — يمكن استخدام كلتا المكتبتين في نفس المشروع من خلال وحدات مختلفة. عند الترحيل من Mockito إلى MockK، يكفي استبدال التعليقات @Mock بـ @MockK وإعادة كتابة كتل when().thenReturn() إلى تنسيق every { }.
الأسئلة المتكررة
Relaxed mock يعيد قيماً افتراضية لجميع الطرق غير المهيأة (قائمة فارغة، 0، null) دون رمي استثناءات. الكائن الوهمي العادي (الصارم) يتطلب stubbing صريح لكل طريقة — وإلا يفشل الاختبار. الكائن الوهمي المرن مناسب للاختبارات السريعة، والصارم للاختبارات الموثوقة.
MockK يدعم إنشاء كائنات وهمية لدوال التمديد من خلال mockkStatic(). هذا ممكن لأن دوال التمديد في Kotlin هي طرق ثابتة مع المستلم كأول معلمة. لكل دالة تمديد، تحتاج إلى تحديد الفئة التي أعلنت فيها.
نعم، MockK يدعم Kotlin Multiplatform (KMP) للكود المشترك. على منصات JVM و Native و JS، يمكن استخدام API المشترك: mockk(), every, verify. هذا يجعل MockK الخيار الوحيد لمشاريع KMP حيث لا يعمل Mockito.
استخدم verifySequence { } — كتلة حيث تحدد الاستدعاءات بدقة بالترتيب المتوقع. إذا كان الترتيب الفعلي مختلفاً، سيرمي verifySequence استثناءً يشير إلى أول استدعاء غير متطابق.
نعم، تقنياً هذا ممكن، لكنه غير موصى به. قد تحدث تعارضات على مستوى أداة bytecode instrumentation (ByteBuddy ضد mockito-inline). إذا كان المشروع يستخدم Mockito بالفعل، يمكن أن يكون الترحيل إلى MockK تدريجياً من خلال عزل الوحدات.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا