Stub (الاختبار الوهمي، الجسر) — كائن اختباري يعيد استجابات محددة مسبقًا لاستدعاءات الوسائل بدلاً من التنفيذ الحقيقي. في تطوير التطبيقات المحمولة، تعزل الاختبارات الوهمية الوحدة المختبرة عن طلبات الشبكة وقواعد البيانات وأنظمة الملفات، مما يسمح بالتحقق من المنطق دون إعداد البيئة. بخلاف mock، لا يتحقق stub من السلوك — هو يقدّم البيانات فقط. لمزيد من التفاصيل، طالع مقال مارتن فاولر حول test doubles.
النقاط الرئيسية
Stub هو كائن اختباري يحل محل التبعية الحقيقية في الاختبار ويعيد قيماً محددة مسبقًا لاستدعاءات محددة. تم إدراج المصطلح في تصنيف Gerard Meszaros (2007) في كتاب «xUnit Test Patterns». ينتمي Stub إلى فئة test doubles — كائنات تستبدل المكوّنات الحقيقية أثناء الاختبار. الهدف الرئيسي للاختبار الوهمي هو تزويد الوحدة المختبرة ببيانات قابلة للتوقع، بإزالة غير اليقين من الأنظمة الخارجية.
مبدأ العمل — يقوم الاختبار بتكوين الاختبار الوهمي قبل التنفيذ: «عندما يتم استدعاء طريقة getUsers()، أعد هذه القائمة من المستخدمين». لا يحتوي الاختبار الوهمي على منطق أعمال، ولا يتحقق من تسلسل الاستدعاءات ولا يسجِّل سجل الاستدعاءات. هو فقط يقف مكان المكوّن الحقيقي ويعيد ما قيل له. في سياق اختبار Android، هذا يعني: عميل OkHttp لا يقوم بطلب حقيقي إلى الخادم، ولكنه يتلقى ردًا من MockWebServer المهيأ كاختبار وهمي.
متى استخدامها — الاختبارات الوهمية مثالية لاختبار طبقة UI (ViewModel، Presenter) ومنطق الأعمال (UseCase، Interactor)، حيث تحتاج إلى التحقق من رد الفعل بيانات محددة: قائمة فارغة، الخادم أعاد خطأ 500، انتهت صلاحية الرمز. أي حالة يتطلب فيها الاختبار حالة إدخال محددة هي مهمة للاختبار الوهمي. يتم إنشاء تكوين خاص لكل سيناريو اختباري، مما يجعل الاختبارات قابلة للقراءة والتوقع.
Gerard Meszaros (2007) في كتاب «xUnit Test Patterns» حدد خمسة أنواع من test doubles: dummy، stub، spy، mock، fake. كل نوع يحل مشكلته. Dummy — يتم تمريره ولكن لا يستخدم. Stub — يعيد البيانات. Spy — يسجِّل الاستدعاءات. Mock — يتحقق من السلوك. Fake — يحتوي على منطق مبسط. يساعد فهم هذا التصنيف المطور على اختيار الأداة المناسبة لكل سيناريو اختباري.
طلبات الشبكة — السيناريو الأكثر شيوعًا لاستخدام الاختبارات الوهمية. يقوم التطبيق بإجراء استدعاءات HTTP للواجهة البرمجية (API)، ويحتاج الاختبار إلى التحقق من رد الفعل لاستجابات مختلفة: JSON ناجح، خطأ 401 (غير مصرّح)، انقضاء المهلة، مصفوفة فارغة. يعمل MockWebServer (OkHttp) على Android و URLProtocol (على iOS) كاختبارات وهمية، معيدة استجابات HTTP محددة مسبقًا دون اتصال حقيقي بالخادم. يعمل ذلك على تسريع الاختبارات من ثوانٍ إلى ملي ثانية.
قاعدة البيانات — لدى Room (Android) و CoreData (iOS) نسخ في الذاكرة، ولكن إعدادها ما زال يستغرق وقتًا. يعيد الاختبار الوهمي بدلاً من المستودع قوائم Entity محضرة مسبقًا دون لمس قاعدة البيانات. هذا فعّال بشكل خاص لاختبار ViewModel، حيث تحتاج إلى التحقق من الترتيب أو التصفية أو تحويل البيانات. يتم تنفيذ الاختبار خلال ميلي ثانية بغض النظر عن حجم البيانات.
خدمات النظام — تتطلب LocationManager، SensorManager، SharedPreferences جهازاً حقيقياً أو محاكياً. يعيد الاختبار الوهمي لـ LocationProvider إحداثيات محددة، ولـ SensorManager — قيماً ثابتة لمقياس التسارع. على iOS، النظير هو CLLocationManager مع تنفيذ مفوض اختباري. بدون الاختبارات الوهمية، تتطلب هذه الاختبارات جهازاً مادياً بظروف محددة.
نظام الملفات والذاكرة المؤقتة — تحميل الصور، تخزين الاستجابات مؤقتًا، العمل مع ملفات التهيئة — جميع هذه العمليات تعتمد على حالة القرص. يعيد الاختبار الوهمي لـ FileManager أو ImageCache النجاح/الخطأ دون قراءة ملفات حقيقية. يقضي ذلك على إخفاقات اختبار خاطئة نتيجة عن عدم تطابق المسارات أو حقوق الوصول على أجهزة المطورين المختلفة.
تقسيم المسؤوليات — تحل ثلاثة أنواع من test doubles مهامًّ مختلفة. Stub: «أعطني البيانات». Mock: «تحقق من أنه تم استدعائي». Fake: «أعمل مثل الحقيقي، ولكن بشكل أبسط». الفرق حاسم لقراءة الاختبارات: إذا استخدم الاختبار mock حيث يحتاج stub، فهو مثقل بأستدعاءات verify غير مرتبطة بالسيناريو المختبر.
| الخصيصة | Stub | Mock | Fake |
|---|---|---|---|
| الغرض | توفير البيانات | التحقق من التفاعل | تنفيذ مبسط |
| المنطق | لا يوجد | لا يوجد | يوجد (ولكن مبسط) |
| التحقق | لا يوجد | نعم (verify) | غير مباشر (عبر الحالة) |
| المرونة | منخفضة — إجابات ثابتة | متوسطة | عالية — المنطق يتكيّف |
| السرعة | قصوى | عالية | متوسطة |
| مثال | MockWebServer يعيد JSON | Mockito.verify(repository).save() | InMemoryRepository مع HashMap |
قاعدة عملية — إذا كان الاختبار يتحقق من البيانات التي تلقاها المكوّن المختبر — استخدم stub. إذا كان الاختبار يتحقق من هل استدعى المكوّن طريقة التبعية بالوسائط الصحيحة — استخدم mock. إذا كنت تريد ببساطة استبدال قاعدة بيانات بجدول تجزئة — هذا fake. يجعل خلط الأنواع في اختبار واحد الاختبار هشاً: عند تغيير التنفيذ، سيتعين عليك إعادة كتابة منطق stub و verify معًا.
Stub مع verify — خطأ شائع حيث يقوم المطور بإعداد stub ثم يضيف verify(stub).method(). لا يجب أن يتم التحقق من stub حسب التعريف — لذلك يوجد mock. إذا كنت تحتاج إلى التحقق من أن طريقة ما تم استدعاؤها بوسائط محددة، استخدم Mockito.mock() بدلاً من Mockito.stub(). يحافظ هذا الفصل على وضوح قصد الاختبار للمطورين الآخرين.
MockWebServer — مكتبة OkHttp لإنشاء اختبارات HTTP وهمية على Android و JVM. تقوم بتشغيل خادم HTTP محلي على منفذ محدد يعترض طلبات عميل OkHttp ويعيد استجابات محددة مسبقًا. يستغرق الإعداد ثلاث أسطر: إنشاء الخادم، إدراج الاستجابة في الطابور، تشغيله. يمكن للاختبار إدراج عدة استجابات بالتسلسل لسيناريوهات التصفح أو إعادة المحاولة.
class UserRepositoryTest {
private val server = MockWebServer()
fun setup() {
server.start(8080)
val client = OkHttpClient.Builder()
.readTimeout(1, TimeUnit.SECONDS)
.build()
}
fun test_user_list_success() {
val json = "[{\"id\":1,\"name\":\"Alice\"}]"
server.enqueue(MockResponse()
.setBody(json)
.setResponseCode(200)
)
val result = repository.getUsers()
assertEquals(1, result.size)
}
fun teardown() {
server.shutdown()
}
}
MockK — بديل لـ Mockito لـ Kotlin مع دعم من الدرجة الأولى لـ كوروتينز ودوال الامتداد والفئات المنتومة. تنشأ الاختبارات الوهمية في MockK عبر coEvery (للدوال suspend) و every (للدوال العادية). بخلاف MockWebServer، يقوم MockK باختبار طرق التبعية الفردية بدلاً من طبقة HTTP بأكملها. هذا مفيد لاختبارات الوحدة لـ UseCase أو Interactor، حيث التبعيات هي تجريدات للمستودعات.
interface UserRepository {
suspend fun getUsers(): List<User>
}
class GetUsersUseCaseTest {
private val repo = mockk<UserRepository>()
private val useCase = GetUsersUseCase(repo)
fun test_empty_list() = runTest {
coEvery { repo.getUsers() } returns emptyList()
val result = useCase.invoke()
assertTrue(result.isEmpty())
coVerify(exactly = 1) { repo.getUsers() }
}
}
أفضل الممارسات — لاختبارات التكامل استخدم MockWebServer (يعترض HTTP الحقيقي)، ولاختبارات الوحدة استخدم MockK (يختبر الواجهات). لا تختبر ما لست تختبره: إذا كان الاختبار يتحقق من Repository، لا تختبر عميل OkHttp داخله — استخدم MockWebServer حقيقياً على مستوى HTTP. تحافظ هذه القاعدة على صلاحية الاختبارات وتقلل هشاشتها أثناء إعادة الهيكلة.
بروتوكولات Swift كاختبارات وهمية — في النهج الأصلي لـ iOS، يتم تنفيذ الاختبار الوهمي عن طريق استبدال هيكل اختباري يتوافق مع بروتوكول التبعية. بدلاً من خدمة NetworkService الحقيقية، يتلقى الاختبار StubNetworkService الذي يعيد بيانات ثابتة. Swift لغة ذات تيميض ثابت، لذا يجب أن يتوافق الاختبار الوهمي مع نفس البروتوكول الذي يتبعه الخدمة الحقيقية. يضمن المجمع أن الاختبار الوهمي ينفِّذ جميع الطرق المطلوبة.
protocol NetworkServiceProtocol {
func fetchUsers() async throws -> [User]
}
struct StubNetworkService: NetworkServiceProtocol {
let result: Result<[User], Error>
func fetchUsers() async throws -> [User] {
try result.get()
}
}
final class UsersViewModelTests: XCTestCase {
func test_success_state() async {
let stub = StubNetworkService(
result: .success([User(name: "Alice")])
)
let vm = UsersViewModel(service: stub)
await vm.load()
XCTAssertEqual(vm.users.count, 1)
}
}
OCMock لـ Objective-C — مكتبة لإنشاء الاختبارات الوهمية والموكات في مشاريع iOS القديمة. يدعم OCMock طرق الاختبار الوهمي بالوسائط والقيم المعادة. تفضّل المشاريع الحديثة بـ Swift نهجًا قائماً على البروتوكولات مع اختبارات وهمية يدوية — يمنح ذلك التحكّم في كل طريقة ولا يتطلب تبعيات خارجية. يبقى OCMock خيارًا للمشاريع حيث لا يكون تحويل جميع التبعيات إلى بروتوكولات مجدياً اقتصادياً.
URLProtocol لاختبارات HTTP الوهمية — آلية نظامية في iOS لاعتراض طلبات الشبكة من خلال فئة فرعية من URLProtocol. يسجِّل الاختبار URLProtocol مخصصًا يعترض URLSession ويعيد استجابات وهمية. الميزة عن الاختبارات اليدوية: لا حاجة لتغيير هيكلة التطبيق — تبقى URLSession حقيقية، ولكن يتم استبدال البيانات على مستوى البروتوكول. العيب: أكثر صعوبة في التصحيح مقارنة بخدمة اختبار وهمي واضح.
الأسئلة الشائعة
Stub يعيد بيانات محددة مسبقًا ولا يتحقق من وقوع الاستدعاء. Mock يتحقق إضافياً من أن الطريقة تم استدعاؤها بالوسائط الصحيحة (verify). Stub يجيب على سؤال «ماذا نعيد؟»، Mock يجيب على «هل تم الاستدعاء؟». استخدم stub للتحقق من الحالة، و mock للتحقق من التفاعل.
Fake مطلوب عندما يحتاج الاختبار إلى تنفيذ عامل (ولو مبسط) — على سبيل المثال، قاعدة بيانات في الذاكرة بدلاً من Room. Stub مناسب للسيناريوهات الفردية ببيانات محددة مسبقًا. إذا كررت نفس stub في 10 اختبارات — على الأرجح أنك تحتاج Fake. يقلِّل Fake التكرار لأن المنطق يعيش في فئة واحدة.
على Android — MockK لكائنات Kotlin (object) يدعم mockkObject()، بما في ذلك الطرق الثابتة لفئات Java عبر mockkStatic(). على iOS — لا يمكن اختبار الطرق الثابتة في Swift مباشرة؛ استخدم البروتوكولات وحقن التبعيات لاستبدال استدعاء static بطريقة مثال لبروتوكول. الاختبارات الوهمية الثابتة هي دين فني ويجب تجنبها في الكود الجديد.
استخدم MockWebServer (OkHttp) — يعمل كخادم HTTP محلي يقوم بإدراج الاستجابات في الطابور. لـ Retrofit، يكفي استبدال عنوان URL الأساسي بـ localhost:8080. لـ Ktor، استخدم MockEngine — آلية مضمنة لاستبدال HttpStatement. كلا النهجين يعملان دون اتصال حقيقي بالإنترنت ويعطيان تحكّمًا كاملاً في رمز الحالة، النص ورئوس الاستجابة.
Spy يغلف كائناً حقيقياً ويسجِّل الاستدعاءات، بينما Stub يستبدل الكائن تماماً بإجابات ثابتة. يسمح Spy باستخدام جزئي للتنفيذ الحقيقي (تعمل الطرق الأخرى كما هي)، بينما لا يفعل stub. إذا كنت تحتاج إلى التحقق من أن طريقة ما تم استدعاؤها ولكن يجب أن يتم تنفيذ جزء من المنطق — استخدم spy، لا stub.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا