التحقق من التفاعلات
في الدرس السابق تعلّمت كيف تضبط الكائنات المُقلِّدة (stubs) في Mockito لتعيد قيمًا محدّدة مسبقًا. يُجيب الضبط عن سؤال "ماذا يُعيد المتعاون؟" — لكنّ طائفة كبيرة من الأخطاء لا علاقة لها بقيم الإعادة على الإطلاق، بل بما إذا كان كودك قد استدعى الدالة الصحيحة، بالمعاملات الصحيحة، العدد الصحيح من المرّات. هذا هو بالضبط ما يُقدّمه التحقق من التفاعلات.
لماذا نتحقق من التفاعلات؟
تخيّل OrderService من المفترض أن يرسل بريدًا إلكترونيًا بعد نجاح الطلب. خدمة البريد تُعيد void — لا شيء لضبطه. الطريقة الوحيدة لإثبات السلوك هي التحقق من أنّ emailService.sendConfirmation(order) استُدعيت فعلًا. بدون هذا التأكيد، يمرّ الاختبار حتى لو حذفت الاستدعاء كليًّا.
اختبارات التفاعل تُكمّل اختبارات الحالة. افضّل التأكيد على الحالة القابلة للملاحظة (قيم الإعادة، الاستثناءات، البيانات المُخزَّنة) أولًا. أضف تأكيدات التفاعل فقط عندما يكون الاستدعاء بذاته هو السلوك المطلوب اختباره — مثل نشر حدث أو التسجيل أو التفويض لمتعاون بلا قيمة إعادة.
الاستخدام الأساسي لـ verify()
أبسط صيغة تؤكد أن دالة مُعيّنة استُدعيت مرّة واحدة بالضبط:
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock EmailService emailService;
@Mock OrderRepository repo;
@InjectMocks OrderService service;
@Test
void sendsConfirmationEmailAfterOrder() {
Order order = new Order(1L, "item-42", 2);
when(repo.save(order)).thenReturn(order);
service.place(order);
// تأكيد أنّ البريد أُرسل مرّة واحدة بالضبط
verify(emailService).sendConfirmation(order);
}
}
الشكل verify(mock).method(args) — دون times() إضافية — يُعادل times(1) ضمنيًّا. إذا لم تُستدعَ الدالة أبدًا أو استُدعيت أكثر من مرّة، يفشل الاختبار برسالة واضحة تُظهر الاستدعاءات الفعلية.
التحكم في عدد الاستدعاءات
تتضمّن Mockito عدة مصانع VerificationMode:
// استُدعيت مرّة على الأقل
verify(emailService, atLeastOnce()).sendConfirmation(any());
// استُدعيت ثلاث مرّات بالضبط
verify(repo, times(3)).findById(anyLong());
// لم تُستدعَ قط
verify(auditLog, never()).warn(anyString());
// استُدعيت مرّتين على الأكثر
verify(cache, atMost(2)).get(anyString());
فضّل never() على تأكيدات الحالة السلبية. إذا كان على كودك ألّا يستدعي متعاونًا معيّنًا في مسار بعينه (مثلًا: لا تُكلِّف العميل حين السلّة فارغة)، فإنّ verify(billing, never()).charge(...) هو أوضح طريقة لتوثيق هذا القيد وإنفاذه.
verifyNoMoreInteractions() وverifyNoInteractions()
بعد جميع استدعاءات verify() المحدّدة، يمكنك التأكد من عدم حدوث أي شيء غير متوقّع:
@Test
void doesNotTouchAuditLogOnHappyPath() {
service.place(order);
verify(repo).save(order);
verify(emailService).sendConfirmation(order);
// أفشل إذا استُدعيت أيّ دالة أخرى على هذه الكائنات المُقلِّدة
verifyNoMoreInteractions(repo, emailService);
}
لا تُفرِط في استخدام verifyNoMoreInteractions(). تطبيقها في كل مكان يجعل الاختبارات هشّة — كل إعادة هيكلة داخلية تمسّ أنماط الاستدعاء تكسر اختبارات غير ذات صلة. احتفظ بها لعقود الواجهات البرمجية العامة حيث تعدّ الاستدعاءات الإضافية غير المتوقّعة خللًا حقيقيًا.
مطابقات المعاملات (Argument Matchers)
أحيانًا تحتاج للتحقق من استدعاء دون الاهتمام بكل معامل. تُقدّم Mockito مكتبة غنية من المطابقات في org.mockito.ArgumentMatchers:
import static org.mockito.ArgumentMatchers.*;
// أي سلسلة نصية غير null
verify(logger).log(anyString());
// قيمة بعينها
verify(repo).findById(eq(42L));
// أي كائن من نوع معيّن
verify(emailService).send(any(EmailMessage.class));
// سلسلة تبدأ ببادئة محدّدة
verify(logger).log(startsWith("[ERROR]"));
// شرط مخصّص باستخدام Lambda
verify(repo).save(argThat(order -> order.getTotal() > 0));
يمكن خلط المطابقات — لكنّ ثمّة قاعدة واحدة: إذا استخدمت مطابقًا لأي معامل، يجب استخدام مطابقات لجميع المعاملات. لا يمكن مزج القيم الحرفية مع المطابقات في نفس الاستدعاء.
// خطأ — يخلط قيمة حرفية مع مطابق
verify(service).process(42L, anyString()); // يرمي InvalidUseOfMatchersException
// صحيح — غلّف القيمة الحرفية في eq()
verify(service).process(eq(42L), anyString());
ArgumentCaptor — التقاط المعاملات للفحص المعمّق
المطابقات ثنائية — الاستدعاء إمّا يطابق أو لا يطابق. عندما تحتاج لفحص المعامل الملتقط بتفصيل (التأكد من حقول متعدّدة، تسجيله للتصحيح، إعادة استخدامه)، استخدم ArgumentCaptor:
@Test
void buildsCorrectEmailMessage() {
Order order = new Order(1L, "item-42", 2);
when(repo.save(order)).thenReturn(order);
service.place(order);
// التقاط ما مُرِّر إلى sendConfirmation
ArgumentCaptor<EmailMessage> captor = ArgumentCaptor.forClass(EmailMessage.class);
verify(emailService).sendConfirmation(captor.capture());
EmailMessage sent = captor.getValue();
assertAll(
() -> assertEquals("orders@example.com", sent.getFrom()),
() -> assertTrue(sent.getBody().contains("item-42")),
() -> assertEquals(order.getCustomerEmail(), sent.getTo())
);
}
مع الأسلوب القائم على التوضيح (يتطلب @ExtendWith(MockitoExtension.class)) يمكنك تعريف الـ captor كحقل:
@Captor ArgumentCaptor<EmailMessage> emailCaptor;
عندما تُستدعى دالة المتعاون نفسها عدة مرّات، يُعيد captor.getAllValues() القائمة الكاملة بترتيب الاستدعاء:
verify(emailService, times(3)).sendConfirmation(emailCaptor.capture());
List<EmailMessage> messages = emailCaptor.getAllValues();
assertEquals("retry@example.com", messages.get(2).getTo());
ترتيب التحقق
بشكل افتراضي، لا يكترث verify() بترتيب الاستدعاءات. عندما يكون التسلسل مهمًّا — مثلًا، يجب مسح الذاكرة المؤقتة قبل الكتابة في قاعدة البيانات — استخدم InOrder:
@Test
void flushesBeforeWriting() {
service.updateWithFlush(entity);
InOrder order = inOrder(cache, repo);
order.verify(cache).flush();
order.verify(repo).save(entity);
}
المفاضلات المهنية
- اختبارات التفاعل هي صندوق أبيض. إنّها تُقرن الاختبار بالتنفيذ لا بالعقد. أي إعادة هيكلة مكافئة خارجيًا لكنّها تُعيد ترتيب الاستدعاءات الداخلية ستكسر اختبارات التفاعل. استخدمها بوعي.
- الـ Captors لتأكيدات مُعقّدة مقبولة، لكن إذا وجدت نفسك تلتقط وتتحقق من خمسة حقول، فكر فيما إذا كان يجب أن يستقبل المتعاون كائن قيمة مخصّصًا يمكنك بناؤه ومقارنته مباشرة بـ
eq().
- فضّل اهتمامًا تحقّقيًّا واحدًا لكل اختبار. خلط تأكيدات الحالة مع تحقّقات تفاعل متعدّدة يُشوّش ما يختبره الاختبار فعلًا.
الخلاصة
verify() هي الأداة للتأكيد على أن كودك استدعى المتعاون كما هو متوقّع. مطابقات المعاملات تتيح كتابة تحقّقات مرنة دون الارتباط بقيم بعينها. ArgumentCaptor يتيح لك استخراج المعامل الفعلي من الكائن المُقلِّد لتُجري عليه تأكيدات تفصيلية. InOrder يُنفّذ التسلسل. استخدم كل هذه الأدوات بتعمّد — الإفراط في التحقق يجعل الاختبارات هشّة وعسيرة الإعادة الهيكلية.