مقدمة: لماذا الاختبارات بدون موكس مثل القهوة بدون كافيين؟
\n\nمرحباً يا زميل! إذا كنت تكتب بلغة Kotlin وتريد ألا يتعطل كودك مع كل commit ثاني، عاجلاً أم آجلاً ستصل إلى الاختبارات. وفي الاختبارات - إلى كائنات mock والفيكستشرز. بدونها، تتحول اختبارات الوحدة إلى اختبارات تكاملية: فأنت لا تختبر فئة واحدة، بل سلسلة كاملة من التبعيات. وإذا تعطلت قاعدة البيانات أو أعادت API خطأ 500 - سيفشل الاختبار، على الرغم من أن كودك قد يكون مثالياً.
\n\nفي هذه المقالة، سنشرح كيفية استخدام كائنات Mock والفيكستشرز في اختبارات Kotlin بشكل صحيح، بحيث تكون الاختبارات سريعة وموثوقة وتخبرك بالحقيقة، وليس "كل شيء معطل، أصلحه". هيا بنا!
\n\nما هو mock ولماذا نحتاجه في Kotlin؟
\n\nكائن Mock هو محاكاة لكائن حقيقي. بدلاً من تشغيل خادم كامل أو قاعدة بيانات، نقوم بإنشاء "بديل" يتصرف كما نطلب. هذا يسمح بعزل الكود المختبر عن العالم الخارجي.
\n\nالمفاهيم الأساسية
\n- \n
- Mock - كائن نتحكم في سلوك دواله. يمكننا تحديد ما سيعيده، والتحقق مما إذا تم استدعاؤه. \n
- Stub - حالة خاصة من mock، تعيد فقط قيماً محددة مسبقاً. لا تتحقق من الاستدعاءات. \n
- Fixture (فيكستشر) - مجموعة من البيانات أو الحالات المعدة مسبقاً اللازمة لتنفيذ الاختبار. على سبيل المثال، مستخدم اختباري أو إعدادات قاعدة بيانات. \n
في عالم Kotlin، هناك لاعبان رئيسيان: MockK (الأصلي لـ Kotlin) و Mockito (الشائع في Java). سنستخدم MockK - فهو يتعامل بشكل أفضل مع coroutines ودوال التوسيع والفئات المختومة (sealed classes).
\n\nإعداد المشروع: إضافة MockK و JUnit 5
\n\nفي البداية، نضيف التبعيات في ملف build.gradle.kts:
dependencies {\n testImplementation("io.mockk:mockk:1.13.8")\n testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")\n}\n\n\nالآن يمكننا كتابة الاختبارات. سننشئ خدمة بسيطة ونختبرها باستخدام الموكس.
\n\nالمثال الأول: mocking المستودع
\n\nتخيل أن لدينا UserService يعتمد على UserRepository:
class UserService(private val repository: UserRepository) {\n fun getUserName(id: Long): String {\n val user = repository.findById(id)\n ?: throw IllegalArgumentException("User not found")\n return user.name\n }\n}\n\n\nالآن نكتب اختباراً باستخدام كائن mock للمستودع:
\n\nimport io.mockk.every\nimport io.mockk.mockk\nimport io.mockk.verify\nimport org.junit.jupiter.api.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertFailsWith\n\nclass UserServiceTest {\n\n private val repository = mockk<UserRepository>()\n private val service = UserService(repository)\n\n @Test\n fun `should return user name when user exists`() {\n // Arrange\n every { repository.findById(1L) } returns User(1L, "Alice")\n\n // Act\n val name = service.getUserName(1L)\n\n // Assert\n assertEquals("Alice", name)\n verify { repository.findById(1L) }\n }\n\n @Test\n fun `should throw exception when user not found`() {\n // Arrange\n every { repository.findById(any()) } returns null\n\n // Act & Assert\n assertFailsWith<IllegalArgumentException> {\n service.getUserName(99L)\n }\n }\n}\n\n\nلاحظ: every يحدد سلوك mock، و verify يتحقق من استدعاء الدالة. هذا هو أساس كائنات mock والفيكستشرز في اختبارات Kotlin.
الفيكستشرز: إعداد البيانات مسبقاً
\n\nعندما يكون هناك العديد من الاختبارات، فإن تكرار إنشاء البيانات هو ممارسة سيئة. هنا تأتي الفيكستشرز للإنقاذ. في Kotlin، من السهل