Test Doubles هي كائنات بديلة تُستخدم في الاختبارات الوحدوية بدلاً من التبعيات الحقيقية. تم تقديم المصطلح بواسطة Gerard Meszaros في كتاب «xUnit Test Patterns» (2007) كمفهوم عام لـ Mock و Stub و Fake و Spy و Dummy. وفقًا Martin Fowler (2024)، تسمح Test Doubles بعزل المكون المختبر عن بيئته، مما يجعل الاختبارات حتمية وسريعة ومستقلة عن الخدمات الخارجية.
النقاط الرئيسية
Test Doubles هو مصطلح من صناعة السيارات (البديل المزدوج)، تم نقله إلى تطوير البرمجيات. كما يحل البديل المزدوج محل الممثل في مشهد خطير، يستبدل Test Double المكون الحقيقي في سيناريو الاختبار. هذا ضروري عندما تكون التبعية الحقيقية غير متاحة، أو بطيئة، أو غير حتمية، أو لها آثار جانبية.
يشمل مفهوم Test Double خمسة أنواع محددة، كل منها يحل مهمته الخاصة. تصنيف Meszaros هو التصنيف المعياري المستخدم في جميع أدلة الاختبار الحديثة. يكمن الاختلاف بين الأنواع في درجة التحكم والتحقق: من مجرد ملء المعاملات (Dummy) إلى التحقق الكامل من تسلسل الاستدعاءات (Mock).
الغرض الرئيسي من Test Doubles هو عزل الوحدة المختبرة. في تطوير التطبيقات المحمولة، تشمل التبعيات الحقيقية خوادم API، وقواعد البيانات، ونظام الملفات، وأجهزة الاستشعار، وخدمات النظام (LocationManager، Camera، Bluetooth). استخدام هذه المكونات مباشرة يجعل الاختبارات بطيئة وهشة ومعتمدة على البيئة. وفقًا لـ Google Testing Blog (2023)، يتم تنفيذ الاختبارات الوحدوية المعزولة جيدًا خلال أجزاء من الثانية، بينما يتم تنفيذ اختبارات التكامل خلال ثوانٍ ودقائق.
تصنيف Gerard Meszaros يشمل خمسة أنواع من Test Doubles، تختلف في السلوك والغرض. فهم الفرق بينها هو أساس الاختبارات الوحدوية المختصة.
Dummy هو كائن يُمرر إلى الطريقة المختبرة لكنه لا يُستخدم أبدًا. Dummy مطلوب فقط لتحقيق توقيع الطريقة. في Kotlin، غالبًا ما يكون null أو emptyList() أو كائنًا مع stubs. يجب ألا يحتوي Dummy على أي منطق — إذا تم استدعاؤه، يجب أن تفشل الاختبارات.
Fake هو تطبيق مبسط لكنه عملي لواجهة. على عكس Mock و Stub، يحتوي Fake على منطق أعمال حقيقي لكن في شكل مبسط. مثال كلاسيكي هو InMemoryUserRepository، الذي يخزن البيانات في HashMap بدلاً من قاعدة بيانات. يُستخدم Fake عندما تحتاج لاختبار منطق يعتمد على الحالة، لكن بدون تكاليف البنية التحتية الحقيقية.
| النوع | الغرض | مثال |
|---|---|---|
| Dummy | ملء معامل | null، كائن فارغ |
| Fake | تطبيق عملي مبسط | InMemoryRepository |
| Stub | إرجاع قيمة ثابتة | when(api.getUser()).thenReturn(user) |
| Spy | تسجيل الاستدعاءات للتحقق | verify(spy).save(user) |
| Mock | التحقق من التفاعل | verify(mock).sendEmail(email) |
Stub يُرجع قيمًا محددة مسبقًا لاستدعاءات محددة. لا يتحقق Stub مما إذا تم استدعاؤه — بل يوفر البيانات فقط. في Mockito، يتم إنشاء Stub عبر when(method).thenReturn(value). Stub مثالي للاختبار عندما تحتاج أن تعيد التبعية قيمة محددة، لكن حقيقة الاستدعاء نفسه ليست مهمة.
Spy هو غلاف حول كائن حقيقي يُسجل جميع الاستدعاءات للتحقق اللاحق. على عكس Mock، يفوض Spy الاستدعاءات إلى الكائن الحقيقي لكنه يسمح بالتحقق من حدوثها. في Mockito، يتم إنشاء Spy عبر spy(realObject). Spy مفيد للـ mocking الجزئي، عندما تريد استخدام كائن حقيقي لكن التحقق من بعض الاستدعاءات.
Mock هو كائن مع توقعات استدعاء محددة مسبقًا. يتحقق Mock من أن أساليب محددة تم استدعاؤها بوسائط محددة وبترتيب محدد. على عكس Stub، يركز Mock على التحقق من السلوك بدلاً من إرجاع البيانات. Mock هو النوع الأكثر قوة والأكثر استخدامًا من Test Double في تطوير التطبيقات المحمولة.
الفرق بين Mock و Stub غالبًا ما يسبب ارتباكًا حتى بين المطورين ذوي الخبرة. الفرق الرئيسي يكمن في الغرض: Stub يتحقق من الحالة (state verification)، Mock يتحقق من السلوك (behavior verification).
Stub يجيب على السؤال: «هل أعاد الكود النتيجة الصحيحة؟». Mock يجيب على السؤال: «هل استدعى الكود الأساليب الصحيحة بالوسائط الصحيحة؟». في تطوير التطبيقات المحمولة، يُستخدم Stub عندما تكون النتيجة مهمة (مثل البيانات من المستودع)، بينما يُستخدم Mock عندما تكون الآثار الجانبية مهمة (مثل إرسال البريد الإلكتروني، الكتابة في قاعدة البيانات).
// Stub: التحقق من الحالة
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: التحقق من السلوك
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
أمثلة عملية لجميع الأنواع الخمسة من Test Doubles في Kotlin باستخدام MockK — أشهر مكتبة mocking لمشاريع Android.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: إرجاع استجابة API ثابتة
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: تحقق من حفظ المستخدم
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context لا يُستخدم داخل Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
يعتمد اختيار نوع Test Double على ما يتم اختباره بالضبط: الحالة، السلوك، أو التكامل. في تطوير التطبيقات المحمولة لنظامي Android و iOS، تم وضع التوصيات التالية.
عند اختبار ViewModel، استخدم Mock للتبعيات التي تنتج آثارًا جانبية (المستودعات، التحليلات، التنقل) و Stub للتبعيات التي تُرجع البيانات (عملاء API، ContentProvider). هذا يسمح بالتحقق من أن ViewModel يتعامل بشكل صحيح مع سيناريوهات النجاح والخطأ.
على مستوى Repository، يُفضل Fake (تطبيقات قاعدة بيانات في الذاكرة) و Stub (استجابات API ثابتة). يسمح Fake باختبار منطق التخزين المؤقت والوضع دون اتصال دون إعداد SQLite. يحاكي Stub حالات HTTP المختلفة: 200، 404، 500، timeout.
الاستخدام غير الصحيح لـ Test Doubles هو أحد أكثر الأسباب شيوعًا للاختبارات الهشة التي تنهار مع كل إعادة هيكلة.
الخطأ الأكثر شيوعًا هو mocking كل شيء. إذا تم استبدال كل تبعية في الاختبار بـ Mock، يتوقف الاختبار عن التحقق من السلوك الحقيقي. يجب استخدام Mock فقط للتبعيات الخارجية (الشبكة، قاعدة البيانات، نظام الملفات، خدمات النظام). لا ينبغي استبدال المكونات الداخلية للتطبيق (Value Object، data class، الأدوات البسيطة).
الخطأ الثاني هو إنشاء Mock دون تحديد التوقعات. إذا تم استدعاء طريقة بدون every / when، يُرجع Mock قيمة افتراضية (null، 0، false). هذا يمكن أن يؤدي إلى اختبارات إيجابية كاذبة، حيث يُرجع Mock بهدوء قيمة null ويفسر الاختبار هذا كسلوك صحيح.
الخطأ الثالث هو التحقق من كل استدعاء لكل Mock. Verify يجب استخدامه فقط للاستدعاءات المهمة بشكل حاسم من منظور منطق الأعمال. التحقق المفرط يجعل الاختبارات هشة: تغيير ترتيب الاستدعاءات في كود الإنتاج يكسر الاختبارات دون تغيير السلوك.
الأسئلة الشائعة
Stub يُرجع البيانات ويتحقق من الحالة (ما تم إرجاعه)، بينما Mock يتحقق من السلوك (أي الأساليب تم استدعاؤها). Stub = «أرجع X»، Mock = «تحقق من أن Y تم استدعاؤه مع الوسيط Z». في الاختبارات الحقيقية، غالبًا ما يعمل كائن واحد كـ Stub و Mock في نفس الوقت.
Fake أفضل من Mock عند اختبار منطق يعتمد على الحالة: التخزين المؤقت، الوضع دون اتصال، المعاملات. Fake (تطبيق في الذاكرة) يسمح باختبار هذه السيناريوهات دون استدعاءات verify هشة. Mock مناسب أكثر للتحقق من إرسال البيانات: التحليلات، push، البريد الإلكتروني.
لمشاريع Android بلغة Kotlin، يُوصى بـ MockK. يدعم coroutines، ووظائف suspend، والفئات المختومة، ووظائف التوسيع دون إعداد إضافي. لمشاريع Java، يظل Mockito هو المعيار — المكتبة الأكثر شعبية مع وثائق شاملة.
لاختبار Kotlin Flow، استخدم مكتبة Turbine مع MockK. تبسط Turbine التحقق من انبعاثات Flow: يمكنك التحقق من ترتيب القيم، وإتمام التدفق، والاستثناءات. يُرجع Stub لـ Flow flowOf(value)، ويتحقق Mock من أن Flow تم تجميعه.
نعم، لكن على مستوى استجابات API، وليس مكونات واجهة المستخدم. تسمح مكتبات MockWebServer (OkHttp) و WireMock بمحاكاة استجابات HTTP في اختبارات واجهة المستخدم. لا ينبغي استبدال مكونات واجهة المستخدم نفسها (Compose، SwiftUI Views) — يتم اختبار سلوكها من خلال اختبارات لقطات الشاشة و Espresso.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.