Mock — ما هو، كائنات mock ومكتبات الاختبارات

المؤلف: IT Sectr نُشر: 2026-04-10 وقت القراءة: 8 دق

Mock هو كائن بديل يحاكي سلوك مكون حقيقي ويسمح بالتحقق من التفاعلات معه. على عكس Stub، الذي يقوم ببساطة بإرجاع قيمة محددة مسبقاً، يسجل Mock حقيقة استدعاء الطريقة والوسائط التي تم تمريرها وعدد الاستدعاءات. وفقاً لـ Mockito (2024)، Mock هو النوع الأكثر شيوعاً من Test Double في مشاريع Java و Kotlin، المستخدم في أكثر من 70% من اختبارات الوحدات في تطبيقات الهاتف المحمول.

النقاط الرئيسية

  • Mock — كائن يتحقق من التفاعل: أي الطرق تم استدعاؤها، وبأي وسائط، وكم مرة
  • Mockito — المكتبة الأكثر شيوعاً لإنشاء Mock في مشاريع Java و Android
  • MockK — بديل لـ Mockito لـ Kotlin مع دعم أصلي للكوروتينات والفئات المغلقة
  • Behavior verification — الفرق الرئيسي بين Mock و Stub: Mock يتحقق من السلوك، وليس الحالة
  • Over-mocking — النمط المعادي الرئيسي: يجب استخدام mock فقط للتبعيات الخارجية

ما هو Mock؟

Mock هو كائن يتم إنشاؤه بواسطة إطار عمل mocking (Mockito, MockK, EasyMock) يحاكي واجهة أو فئة ويسجل جميع استدعاءات طرقه. يحدد المطور التوقعات: سيتم استدعاء الطريقة X بالوسائط Y وستعيد Z. بعد تنفيذ الاختبار، يتحقق Mock من أن التوقعات تطابقت مع الاستدعاءات الفعلية.

المصطلح يأتي من الاستعارة المسرحية لـ Test Doubles: Mock هو «محاكي» لا يقف فقط على المسرح (مثل Dummy) بل يؤدي دوراً ويتحقق مما إذا كان التفاعل معه صحيحاً. إذا لم يستدع الكود قيد الاختبار الطريقة التي توقعها Mock، أو استدعاها بوسائط غير صحيحة — يفشل الاختبار برسالة توقع مخالف.

كيف يعمل Mock

يتم إنشاء Mock من خلال مصنع إطار العمل: mockk<MyInterface>() أو Mockito.mock(MyClass.java). يولد إطار العمل كائن وكيل يعترض جميع استدعاءات الطرق. تتم مقارنة كل استدعاء مع التوقعات المحددة مسبقاً. إذا تطابق الاستدعاء مع توقعات — يتم إرجاع القيمة المحددة. إذا لم يتطابق — يعيد Mock قيمة افتراضية أو يطرح استثناءً، حسب التكوين.

متى يكون Mock ضرورياً

Mock ضروري عندما يتفاعل الكود قيد الاختبار مع مكونات لها آثار جانبية: إرسال البيانات إلى الخادم، الكتابة في قاعدة البيانات، التسجيل، analytics، التنقل، عرض حوارات النظام. بدون Mock، لا يمكن التحقق من هذه التفاعلات دون تشغيل بنية تحتية حقيقية. وفقاً لمدونة Google Testing Blog، Mock هو الطريقة الوحيدة للتحقق من أن التطبيق أرسل بالفعل حدث analytics دون تشغيل خادم اختبار.

Mock و Stub: مقارنة مفصلة

الفرق بين Mock و Stub هو أحد أكثر المواضيع مناقشة في الاختبارات. كلا النوعين يستبدلان تبعية حقيقية، ولكن بطرق مختلفة جوهرياً.

المعيارMockStub
السؤال الرئيسيهل تم استدعاء الطريقة؟ما النتيجة التي تم إرجاعها؟
التحققالسلوك (verify)الحالة (assert)
إرجاع البياناتاختياريإلزامي
مثالverify(analytics).logEvent(“click”)assertEquals(5, repository.getCount())
متى تستخدمالآثار الجانبيةإرجاع البيانات

قاعدة عملية: Mock أم لا

اختبار بسيط للاختيار: اسأل نفسك «إذا حذفت هذا السطر من الكود، هل سيفشل الاختبار؟» إذا كان الاختبار يتحقق من قيمة الإرجاع — تحتاج Stub (تحقق قائم على assert). إذا كان الاختبار يتحقق من أن الكود استدعى طريقة بالوسائط الصحيحة — تحتاج Mock (تحقق قائم على verify). هذا الانقسام يتبع نمط Command-Query Separation: الطرق التي تغير الحالة (commands) تحتاج Mock؛ الطرق التي تعيد البيانات (queries) تحتاج Stub.

Mockito و MockK: مقارنة المكتبات

الاختيار بين Mockito و MockK هو واحد من أول القرارات عند إعداد مجموعة اختبارات لمشروع Android بلغة Kotlin. كلا المكتبتين تؤديان نفس المهمة، ولكن بأساليب مختلفة تجاه ميزات Kotlin الخاصة.

Mockito: الكلاسيكي المُجرب

Mockito هو المعيار الفعلي لمشاريع Java. الإصدار 5.x يدعم mocking للفئات النهائية والطرق الثابتة والمنشئات بفضل MockMaker المدمج. لمشاريع Kotlin، يتطلب Mockito إعداداً إضافياً: امتدادات mockito-kotlin لتحسين الصياغة، و mockito-inline للفئات النهائية. لا يدعم Mockito كوروتينات Kotlin ووظائف suspend بدون محولات إضافية.

MockK: نهج Kotlin أولاً

MockK تم إنشاؤه خصيصاً لـ Kotlin. يدعم بشكل أصلي الكوروتينات (coEvery, coVerify)، والفئات المغلقة، وفئات البيانات، ومفردات الكائنات، ووظائف الامتداد. تستخدم صياغة MockK DSL مع كتل لامبدا، مما يبدو طبيعياً في كود Kotlin. كما يدعم MockK mocking الخصائص دون إعداد إضافي — وهذا مهم لمشاريع Android التي تستخدم LiveData و StateFlow و 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

مقارنة الأداء

تظهر المعايير (JVM Benchmark, 2024) أن MockK ينشئ كائنات mock أسرع بنسبة 15–20% من Mockito لمشاريع Kotlin بفضل العمل المباشر مع bytecode Kotlin بدلاً من Java Reflections. للمشاريع التي تحتوي على آلاف اختبارات الوحدات، يمكن أن يكون الفرق في سرعة البناء ملحوظاً: يوفر MockK 30–60 ثانية في تشغيل كامل للاختبارات في المشاريع الكبيرة.

أمثلة اختبارات Mock في Kotlin

دعنا نستعرض ثلاثة سيناريوهات: اختبار ViewModel مع تبعيات Mock، واختبار UseCase مع التحقق من استدعاءات API، واختبار الكوروتينات مع coVerify.

مثال 1: ViewModel مع 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") }
    }
}

مثال 2: UseCase مع تحقق غير متزامن

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)
    }
}

مثال 3: التحقق من الوسائط باستخدام 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])
    }
}

أفضل ممارسات اختبارات Mock

الاستخدام الفعال لـ Mock في تطوير الهاتف المحمول يتطلب الانضباط. انتهاك هذه القواعد يحول الاختبارات إلى عوائق هشة تنكسر مع كل إعادة هيكلة.

Mock فقط لحدود التطبيق الخارجية

قاعدة صارمة: يجب إنشاء Mock فقط للتبعيات التي تعبر حدود التطبيق: عملاء API، قواعد البيانات، نظام الملفات، خدمات النظام (LocationManager, BluetoothAdapter, Camera). الفئات الداخلية للتطبيق — كيانات المجال، Value Objects، الأدوات المساعدة البسيطة — لا يجب استبدالها بـ Mock. يتم اختبار سلوكها من خلال كائنات حقيقية.

assert / verify واحد لكل اختبار

يجب أن يحتوي كل اختبار على فحص منطقي واحد تماماً — إما verify (لـ Mock) أو assert (لـ Stub). لا تخلط بين التحقق من الحالة والسلوك في اختبار واحد. إذا كنت بحاجة للتحقق من استدعاء API ونتيجته — أنشئ اختبارين منفصلين بأسماء مختلفة. هذه القاعدة، المعروفة باسم «assert واحد لكل اختبار»، تعود إلى توصيات Kent Beck (2002).

  • استخدم relaxUnitFun = true في MockK للطرق التي تعيد Unit — وإلا سيرمي Mock استثناءً عند استدعاء غير محدد
  • اقتصر على الاستدعاءات الحرجة فقط — لا تتحقق من كل getter و setter، فهذا يجعل الاختبارات هشة
  • طبق ArgumentMatchers بشكل مدروس — any() يخفي تفاصيل مهمة إذا كانت الوسيطة حرجة لمنطق الأعمال
  • لا تفرط في استخدام verifyNoMoreInteractions — هذه الطريقة تجعل الاختبارات جامدة بشكل غير ضروري تجاه أي تغييرات في كود الإنتاج
  • استخدم @MockkAnnotations للتهيئة التلقائية لكائنات Mock — هذا يقلل من الكود المتكرر ويحسن قابلية القراءة

تقنيات متقدمة لاختبارات Mock

إلى جانب mocking الأساسي، هناك تقنيات متقدمة تحل مهاماً محددة في تطوير الهاتف المحمول: اختبار تعدد الخيوط، والتحقق من حالة Flow، و mocking الجزئي للكائنات الحقيقية.

Mock جزئي مع spyK

Spy (أو mock جزئي) يسمح بإنشاء كائن يفوض الاستدعاءات إلى التنفيذ الحقيقي ولكنه يسمح بتجاوز طرق فردية. في MockK، يتم إنشاء spyk بناءً على مثيل فئة حقيقي: val repo = spyk(InMemoryUserRepository()). الاستدعاءات ذات التوقعات المحددة عبر every تمر عبر Mock؛ والباقي يمر عبر الكائن الحقيقي. Spy مفيد بشكل خاص لاختبار الكود القديم حيث لم يتم تطبيق حقن التبعيات بعد وتحتاج فقط إلى تجاوز طريقة واحدة.

اختبار StateFlow باستخدام Turbine

في مشاريع Android الحديثة باستخدام Jetpack Compose، يعرض ViewModel الحالة من خلال StateFlow. يسمح MockK بـ mocking تبعيات Flow، ومكتبة Turbine تبسط التحقق من الانبعاثات. النمط الكلاسيكي: MockK لـ UseCase يعيد Flow، و Turbine للتحقق من انبعاثات ViewModel. هذه المجموعة موصى بها من قبل وثائق Android Testing (Google, 2024) للمشاريع التي تستخدم 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()
        }
    }
}

الأسئلة الشائعة

كيف يختلف Mock عن Mockito؟

Mock هو مفهوم، نوع من Test Double يتحقق من السلوك. Mockito هي مكتبة لإنشاء كائنات Mock في Java و Android. مكتبات أخرى: MockK (Kotlin)، EasyMock (Java)، Cuckoo (iOS).

كيف يعمل Mock مع كوروتينات Kotlin؟

لاختبار دوال suspend باستخدام Mock، استخدم MockK (coEvery / coVerify) أو Mockito مع mockito-kotlin. يدعم MockK الكوروتينات بشكل أصلي: coEvery يحدد سلوك دالة suspend، و coVerify يتحقق من استدعائها داخل كوروتين. يجب تنفيذ جميع استدعاءات suspend داخل runTest (kotlinx-coroutines-test).

هل يمكن لـ Mock إرجاع قيم مختلفة عند الاستدعاءات المتكررة؟

نعم. في MockK، استخدم returnsMany: every { api.getData() } returnsMany listOf(response1, response2). في Mockito — سلسلة thenReturn(value1).thenReturn(value2). هذا مفيد لاختبار السلوك مع استدعاءات متسلسلة تعيد ردوداً مختلفة.

كيفية مسح حالة Mock بين الاختبارات؟

في MockK، استخدم التعليق التوضيحي @MockK مع relaxed = true واستدعِ clearMocks(mock) في طريقة @After. في MockitoMockito.reset(mock). أفضل ممارسة: إنشاء Mock جديد لكل اختبار عبر @Before لإزالة التداخل بين الاختبارات.

كيف يتعامل Mock مع الفئات المغلقة في Kotlin؟

MockK يعمل بشكل صحيح مع الفئات المغلقة: every { useCase() } returns Result.Success(data). Mockito لا يدعم الفئات المغلقة مباشرة، مما يتطلب حلولاً بديلة. هذا أحد الأسباب التي تجعل MockK موصى به لمشاريع Kotlin بدلاً من Mockito.

الملخص

  • Mock — نوع Test Double يتحقق من السلوك (verify)، وليس الحالة (assert) للتبعيات
  • Mockito — المعيار لـ Java/Android، MockK — الاختيار Kotlin-first مع دعم الكوروتينات والفئات المغلقة
  • القاعدة الرئيسية: Mock للحدود الخارجية (الشبكة، قاعدة البيانات، خدمات النظام)، كائنات حقيقية للفئات الداخلية
  • Over-mocking — النمط المعادي الرئيسي: الاستبدال المفرط للتبعيات يجعل الاختبارات هشة وغير مفيدة
  • اختبار واحد — فحص منطقي واحد: verify لـ Mock أو assert لـ Stub، وليس كلاهما في نفس الاختبار
  • ArgumentCaptor / slot — الطريقة الصحيحة للتحقق من وسائط استدعاء Mock بدلاً من any() الأعمى
  • MockK موصى به لمشاريع Kotlin: coEvery و coVerify يعملان بشكل أصلي مع الكوروتينات بدون محولات إضافية

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا