لماذا تتعطل اختبارات Java الخاصة بك (وكيفية إصلاح ذلك)
\n\nلقد كتبت كوداً مثالياً. الخدمة تعمل، المنطق لا تشوبه شائبة، والأخطاء موجودة فقط في مشاريع الآخرين. ولكن عندما تقوم بتشغيل الاختبارات... هناك خطأ في الاتصال بقاعدة البيانات، وهنا واجهة API خارجية لا تستجيب، وفي الاختبار الثالث هناك NullPointerException لأن التهيئة لم تتم بشكل صحيح. هل هذا مألوف؟
\n\nالمشكلة هي أن الاختبارات التقليدية تعتمد على البيئة الخارجية. قاعدة البيانات، خدمات REST، نظام الملفات - كل هذا يجعل الاختبارات هشة وبطيئة وغير متوقعة. الحل هو كائنات mock و الـ fixtures. هذه ليست مجرد كلمات رنانة، بل أدوات تحول الفوضى إلى نظام.
\n\nفي هذه المقالة، سنتعرف على كيفية استخدام mock و fixtures بشكل صحيح في اختبارات Java من خلال أمثلة واقعية. ستتعلم الفرق بين stub و mock، وكيفية إعداد fixtures لسيناريوهات معقدة، ولماذا Mockito هو أفضل صديق لك. هيا بنا!
\n\nكائنات Mock: محاكاة بدون ألم
\n\nما هو mock ولماذا هو مطلوب؟
\n\nكائن mock هو محاكاة لمكون حقيقي يتصرف كما تطلب أنت. بدلاً من تشغيل قاعدة بيانات أو استدعاء واجهة API خارجية، تقوم بإنشاء "مزيف" يعيد بيانات محددة مسبقاً.
\n\nتخيل: أنت تختبر خدمة ترسل إشعارات البريد الإلكتروني. للاختبار الحقيقي، تحتاج إلى تشغيل خادم بريد، إعداد SMTP، التحقق من قائمة الانتظار... أما مع كائن mock، فأنت فقط تتحقق من أن الخدمة استدعت طريقة الإرسال بالمعاملات الصحيحة. سريع، نظيف، وقابل للتحكم.
\n\nMock vs Stub: ما الفرق؟
\n\nغالباً ما يخلط المبتدئون بين هذين المفهومين. دعنا نوضح الأمور:
\n\n- \n
- Stub - يعيد بيانات ثابتة. أنت تقول: "عند استدعاء طريقة getUser()، أعد كائن User باسم 'John'". الـ Stub لا يتحقق من عدد مرات استدعائه. \n
- Mock - لا يعيد البيانات فحسب، بل يتحقق أيضاً من السلوك. يمكنك أن تقول: "تأكد من أن طريقة sendEmail() قد تم استدعاؤها مرة واحدة بالضبط مع عنوان محدد". \n
ببساطة: stub هو "أعطني بيانات"، mock هو "أعطني بيانات وتحقق من كيفية استخدامي". عملياً، يتم استخدام كائنات mock في الغالب لأنها توفر تحكماً أكبر.
\n\nكتابة أول mock باستخدام Mockito
\n\nMockito هي المكتبة الأكثر شيوعاً للـ mocks في Java. تتكامل مع JUnit 5 و Spring Boot، وبناء الجملة بسيط لدرجة أنك ستتذكره في خمس دقائق.
\n\nلنفترض أن لديك خدمة OrderService تعتمد على مستودع OrderRepository. بدلاً من المستودع الحقيقي، سننشئ mock:
import static org.mockito.Mockito.*;\nimport org.junit.jupiter.api.Test;\nimport org.mockito.Mock;\nimport org.mockito.junit.jupiter.MockitoExtension;\nimport org.junit.jupiter.api.extension.ExtendWith;\n\n@ExtendWith(MockitoExtension.class)\npublic class OrderServiceTest {\n\n @Mock\n private OrderRepository orderRepository;\n\n @Test\n void testCreateOrder() {\n // إنشاء كائن mock\n OrderService orderService = new OrderService(orderRepository);\n \n // تكوين سلوك mock\n when(orderRepository.save(any(Order.class)))\n .thenReturn(new Order(1L, "test@example.com", 100.0));\n \n // استدعاء الطريقة المختبرة\n Order result = orderService.createOrder("test@example.com", 100.0);\n \n // التحقق من النتيجة\n assertThat(result.getId()).isEqualTo(1L);\n // التحقق من أن طريقة save قد تم استدعاؤها مرة واحدة بالضبط\n verify(orderRepository, times(1)).save(any(Order.class));\n }\n}\n\nفي هذا المثال، قمنا بـ:
\n- \n
- إنشاء mock لـ
OrderRepositoryباستخدام التعليق التوضيحي@Mock. \n - تكوين أنه عند استدعاء
save()يجب إعادة طلب تم إنشاؤه مسبقاً. \n - التحقق