الفصل 3: المحاكاة باستخدام Mockito
مقدمة
كيف تختبر UserService الذي يعتمد على UserRepository؟ بدون قاعدة البيانات، يفشل الاختبار. الحل: محاكاة المستودع لمحاكاة سلوكه.
🎯 أهداف الفصل
في نهاية هذا الفصل، ستعرف:
- ✅ فهم لماذا ومتى يتم المحاكاة
- ✅ إنشاء محاكيات باستخدام @Mock و@InjectMocks
- ✅ تكوين السلوك باستخدام when().thenReturn()
- ✅ التحقق من الاستدعاءات باستخدام verify()
- ✅ استخدام مطابقة المعاملات
1. لماذا المحاكاة؟
المشكلة
// هذه الفئة تعتمد على UserRepository
@Service
public class UserService {
private final UserRepository userRepository;
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
// كيف تختبر بدون قاعدة بيانات؟
@Test
void shouldFindUserById() {
UserService service = new UserService(???); // نحتاج إلى مستودع!
}
الحل: المحاكاة
@Test
void shouldFindUserById() {
// إنشاء مستودع وهمي
UserRepository mockRepo = mock(UserRepository.class);
// إبلاغه بما يعيده
when(mockRepo.findById(1L)).thenReturn(Optional.of(new User("John")));
// اختبار الخدمة
UserService service = new UserService(mockRepo);
User user = service.findById(1L);
assertEquals("John", user.getName());
}
2. التكوين الأساسي
باستخدام التعليقات التوضيحية (موصى به)
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class) // ← تفعيل Mockito
class UserServiceTest {
@Mock // ← إنشاء محاكاة
private UserRepository userRepository;
@InjectMocks // ← حقن المحاكيات
private UserService userService;
@Test
void shouldFindUserById() {
// when/then هنا
}
}
تدفق الحقن
@Mock userRepository ─────┐
├──→ @InjectMocks userService
@Mock anotherDep ─────────┘
3. when().thenReturn()
إرجاع قيمة
@Test
void shouldReturnUser() {
// تكوين المحاكاة
User mockUser = new User(1L, "John", "john@email.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// اختبار
User result = userService.findById(1L);
// التحقق
assertEquals("John", result.getName());
}
إرجاع قيم مختلفة
@Test
void shouldReturnDifferentUsers() {
when(userRepository.findById(1L)).thenReturn(Optional.of(new User("John")));
when(userRepository.findById(2L)).thenReturn(Optional.of(new User("Jane")));
when(userRepository.findById(99L)).thenReturn(Optional.empty());
assertEquals("John", userService.findById(1L).getName());
assertEquals("Jane", userService.findById(2L).getName());
assertThrows(UserNotFoundException.class, () -> userService.findById(99L));
}
استدعاءات متتالية
@Test
void shouldReturnDifferentValuesOnConsecutiveCalls() {
when(userRepository.count())
.thenReturn(0L)
.thenReturn(1L)
.thenReturn(2L);
assertEquals(0L, userRepository.count()); // الاستدعاء الأول
assertEquals(1L, userRepository.count()); // الاستدعاء الثاني
assertEquals(2L, userRepository.count()); // الاستدعاء الثالث
assertEquals(2L, userRepository.count()); // متابعة: آخر قيمة
}
4. thenThrow() - محاكاة الاستثناءات
@Test
void shouldThrowException() {
when(userRepository.findById(anyLong()))
.thenThrow(new DatabaseException("Connection failed"));
assertThrows(DatabaseException.class, () -> userService.findById(1L));
}
@Test
void shouldThrowThenSucceed() {
when(userRepository.save(any()))
.thenThrow(new RuntimeException("First call fails"))
.thenReturn(new User("Saved"));
// الاستدعاء الأول: استثناء
assertThrows(RuntimeException.class, () -> userService.save(new User()));
// الاستدعاء الثاني: نجاح
User saved = userService.save(new User());
assertNotNull(saved);
}
5. مطابقة المعاملات
any(), anyLong(), anyString()
@Test
void shouldAcceptAnyId() {
when(userRepository.findById(anyLong())) // أي Long
.thenReturn(Optional.of(new User("Generic")));
assertEquals("Generic", userService.findById(1L).getName());
assertEquals("Generic", userService.findById(999L).getName());
}
المطابقات المتاحة
any() // أي كائن
any(User.class) // أي User
anyLong() // أي long
anyInt() // أي int
anyString() // أي String
anyList() // أي List
anyMap() // أي Map
eq(value) // تطابق دقيق (مع مطابقة أخرى)
isNull() // Null
isNotNull() // غير Null
قاعدة مهمة: الكل أو لا شيء
// ❌ خطأ: خلط بين المطابقة والقيمة الدقيقة
when(service.process(anyString(), "fixed")) // خطأ!
// ✅ صحيح: استخدام eq() للقيمة الدقيقة
when(service.process(anyString(), eq("fixed")))
argThat() - مطابقة مخصصة
@Test
void shouldMatchCustomCondition() {
when(userRepository.save(argThat(user ->
user.getName() != null && user.getName().length() > 3
))).thenReturn(new User("Saved"));
// يعمل
User saved = userService.save(new User("John"));
assertNotNull(saved);
// لا يتطابق -> يعيد null (السلوك الافتراضي للمحاكاة)
User notSaved = userService.save(new User("Jo"));
assertNull(notSaved);
}
6. verify() - التحقق من الاستدعاءات
التحقق البسيط
@Test
void shouldCallRepository() {
userService.delete(1L);
// التحقق من أن deleteById تم استدعاؤه مع 1L
verify(userRepository).deleteById(1L);
}
عدد الاستدعاءات
@Test
void shouldCallRepositoryCorrectTimes() {
userService.processAll();
verify(userRepository, times(3)).save(any()); // بالضبط 3 مرات
verify(userRepository, atLeast(2)).findById(any()); // على الأقل مرتين
verify(userRepository, atMost(5)).count(); // على الأكثر 5 مرات
verify(userRepository, never()).delete(any()); // لم يتم استدعاؤه أبداً
}
ترتيب الاستدعاءات
@Test
void shouldCallInOrder() {
userService.createAndNotify(new User("John"));
InOrder inOrder = inOrder(userRepository, emailService);
// التحقق من الترتيب
inOrder.verify(userRepository).save(any()); // أولاً الحفظ
inOrder.verify(emailService).sendWelcome(any()); // ثم البريد الإلكتروني
}
verifyNoMoreInteractions
@Test
void shouldOnlyCallSpecificMethods() {
userService.findById(1L);
verify(userRepository).findById(1L);
verifyNoMoreInteractions(userRepository); // لا استدعاءات أخرى
}
7. @Spy - محاكاة جزئية
الفرق بين Mock وSpy
// Mock: جميع الاستدعاءات محاكية (تعيد null/0/false افتراضياً)
@Mock
private UserService mockService;
// Spy: تنفيذ حقيقي، لكن يمكن تجاوز بعض الطرق
@Spy
private UserService spyService;
الاستخدام
@ExtendWith(MockitoExtension.class)
class PartialMockTest {
@Spy
private ArrayList<String> spyList = new ArrayList<>();
@Test
void testSpy() {
// استدعاء حقيقي
spyList.add("one");
spyList.add("two");
assertEquals(2, spyList.size());
// تجاوز السلوك
when(spyList.size()).thenReturn(100);
assertEquals(100, spyList.size());
// العنصر موجود فعلاً
assertEquals("one", spyList.get(0));
}
}
doReturn() مقابل when() مع Spy
@Spy
private UserService userService;
@Test
void testSpy() {
// ❌ مشكلة: when() تستدعي الطريقة الحقيقية أولاً!
when(userService.riskyMethod()).thenReturn("safe");
// ✅ الحل: doReturn() لا تستدعي الطريقة الحقيقية
doReturn("safe").when(userService).riskyMethod();
}
8. التقاط المعاملات
ArgumentCaptor
@Test
void shouldCaptureArgument() {
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
userService.register("John", "john@email.com", "password");
verify(userRepository).save(userCaptor.capture());
User savedUser = userCaptor.getValue();
assertEquals("John", savedUser.getName());
assertEquals("john@email.com", savedUser.getEmail());
assertNotNull(savedUser.getPassword()); // مشفر
}
باستخدام التعليق التوضيحي
@Captor
private ArgumentCaptor<User> userCaptor;
@Test
void shouldCaptureMultipleArgs() {
userService.saveAll(List.of(new User("A"), new User("B")));
verify(userRepository, times(2)).save(userCaptor.capture());
List<User> capturedUsers = userCaptor.getAllValues();
assertEquals(2, capturedUsers.size());
assertEquals("A", capturedUsers.get(0).getName());
assertEquals("B", capturedUsers.get(1).getName());
}
9. مثال كامل
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private ProductService productService;
@Mock
private EmailService emailService;
@InjectMocks
private OrderService orderService;
@Captor
private ArgumentCaptor<Order> orderCaptor;
@Test
void shouldCreateOrderSuccessfully() {
// الترتيب
Product product = new Product(1L, "Laptop", 1000.0);
when(productService.findById(1L)).thenReturn(Optional.of(product));
when(orderRepository.save(any())).thenAnswer(inv -> {
Order order = inv.getArgument(0);
order.setId(100L);
return order;
});
// التنفيذ
Order order = orderService.createOrder(1L, 2, "john@email.com");
// التحقق
assertNotNull(order);
assertEquals(100L, order.getId());
assertEquals(2000.0, order.getTotal());
// التحقق
verify(productService).findById(1L);
verify(orderRepository).save(orderCaptor.capture());
verify(emailService).sendOrderConfirmation(eq("john@email.com"), any());
Order savedOrder = orderCaptor.getValue();
assertEquals(2, savedOrder.getQuantity());
}
@Test
void shouldThrowWhenProductNotFound() {
// الترتيب
when(productService.findById(anyLong())).thenReturn(Optional.empty());
// التنفيذ والتحقق
assertThrows(ProductNotFoundException.class,
() -> orderService.createOrder(999L, 1, "test@email.com"));
// التحقق من عدم حفظ أي طلب
verify(orderRepository, never()).save(any());
verify(emailService, never()).sendOrderConfirmation(anyString(), any());
}
}
10. ملخص
التعليقات التوضيحية
| التعليق التوضيحي | الوصف |
|---|---|
@Mock | إنشاء محاكاة |
@InjectMocks | حقن المحاكيات في الفئة المختبرة |
@Spy | محاكاة جزئية (تنفيذ حقيقي) |
@Captor | التقاط المعاملات |
@ExtendWith(MockitoExtension.class) | تفعيل Mockito |
الطرق الرئيسية
// التكوين
when(mock.method()).thenReturn(value);
when(mock.method()).thenThrow(exception);
doReturn(value).when(spy).method();
// التحقق
verify(mock).method();
verify(mock, times(3)).method();
verify(mock, never()).method();
verifyNoMoreInteractions(mock);
// المطابقات
any(), anyLong(), anyString()
eq(value)
argThat(condition)
🎯 الخطوة التالية
Mockito مثالي للاختبارات الوحدوية، لكن في بعض الأحيان تحتاج إلى الاختبار باستخدام موارد حقيقية. هذا هو دور الاختبارات التكاملية.