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 ضروري عندما يتفاعل الكود قيد الاختبار مع مكونات لها آثار جانبية: إرسال البيانات إلى الخادم، الكتابة في قاعدة البيانات، التسجيل، 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 يدعم mocking للفئات النهائية والطرق الثابتة والمنشئات بفضل MockMaker المدمج. لمشاريع Kotlin، يتطلب Mockito إعداداً إضافياً: امتدادات mockito-kotlin لتحسين الصياغة، و mockito-inline للفئات النهائية. لا يدعم Mockito كوروتينات Kotlin ووظائف suspend بدون محولات إضافية.
MockK تم إنشاؤه خصيصاً لـ Kotlin. يدعم بشكل أصلي الكوروتينات (coEvery, coVerify)، والفئات المغلقة، وفئات البيانات، ومفردات الكائنات، ووظائف الامتداد. تستخدم صياغة MockK DSL مع كتل لامبدا، مما يبدو طبيعياً في كود Kotlin. كما يدعم MockK 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 أسرع بنسبة 15–20% من Mockito لمشاريع Kotlin بفضل العمل المباشر مع bytecode Kotlin بدلاً من Java Reflections. للمشاريع التي تحتوي على آلاف اختبارات الوحدات، يمكن أن يكون الفرق في سرعة البناء ملحوظاً: يوفر MockK 30–60 ثانية في تشغيل كامل للاختبارات في المشاريع الكبيرة.
دعنا نستعرض ثلاثة سيناريوهات: اختبار ViewModel مع تبعيات Mock، واختبار UseCase مع التحقق من استدعاءات API، واختبار الكوروتينات مع 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).
إلى جانب mocking الأساسي، هناك تقنيات متقدمة تحل مهاماً محددة في تطوير الهاتف المحمول: اختبار تعدد الخيوط، والتحقق من حالة Flow، و mocking الجزئي للكائنات الحقيقية.
Spy (أو mock جزئي) يسمح بإنشاء كائن يفوض الاستدعاءات إلى التنفيذ الحقيقي ولكنه يسمح بتجاوز طرق فردية. في MockK، يتم إنشاء spyk بناءً على مثيل فئة حقيقي: val repo = spyk(InMemoryUserRepository()). الاستدعاءات ذات التوقعات المحددة عبر every تمر عبر Mock؛ والباقي يمر عبر الكائن الحقيقي. Spy مفيد بشكل خاص لاختبار الكود القديم حيث لم يتم تطبيق حقن التبعيات بعد وتحتاج فقط إلى تجاوز طريقة واحدة.
في مشاريع Android الحديثة باستخدام Jetpack Compose، يعرض ViewModel الحالة من خلال StateFlow. يسمح MockK بـ mocking تبعيات 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 الكوروتينات بشكل أصلي: coEvery يحدد سلوك دالة suspend، و coVerify يتحقق من استدعائها داخل كوروتين. يجب تنفيذ جميع استدعاءات 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 يعمل بشكل صحيح مع الفئات المغلقة: every { useCase() } returns Result.Success(data). Mockito لا يدعم الفئات المغلقة مباشرة، مما يتطلب حلولاً بديلة. هذا أحد الأسباب التي تجعل MockK موصى به لمشاريع Kotlin بدلاً من Mockito.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا