انتقل إلى المحتوى الرئيسي

اختبار: اختبارات Spring Boot

اختبر معرفتك بـ JUnit 5 و Mockito والاختبارات التكاملية.

30 سؤالاً - انقر على كل سؤال لكشف الإجابة.


الأسئلة 1-10: JUnit 5​

السؤال 1: ما هي التعليقات التوضيحية الأساسية لـ JUnit 5؟

الإجابة:

التعليق التوضيحيالوصف
@Testيحدد طريقة اختبار
@BeforeEachقبل كل اختبار
@AfterEachبعد كل اختبار
@BeforeAllقبل جميع الاختبارات (static)
@AfterAllبعد جميع الاختبارات (static)
@Disabledيعطل اختبار
@DisplayNameاسم قابل للقراءة

السؤال 2: كيف يتم هيكلة اختبار باستخدام نمط AAA؟

الإجابة: Arrange-Act-Assert:

@Test
void shouldCalculateTotal() {
// Arrange (Given)
Order order = new Order();
order.addItem(new Item("Book", 10.0));
order.addItem(new Item("Pen", 2.0));

// Act (When)
double total = order.calculateTotal();

// Assert (Then)
assertEquals(12.0, total);
}

السؤال 3: ما هي التأكيدات التي يمكن استخدامها مع AssertJ؟

الإجابة:

import static org.assertj.core.api.Assertions.*;

assertThat(result).isEqualTo(expected);
assertThat(list).hasSize(3);
assertThat(list).contains("item");
assertThat(user.getName()).isNotBlank();
assertThat(price).isGreaterThan(0);
assertThatThrownBy(() -> service.process(null))
.isInstanceOf(IllegalArgumentException.class);

السؤال 4: كيف يتم اختبار استثناء؟

الإجابة:

// JUnit 5
@Test
void shouldThrowException() {
assertThrows(IllegalArgumentException.class, () -> {
service.process(null);
});
}

// AssertJ
@Test
void shouldThrowException() {
assertThatThrownBy(() -> service.process(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("cannot be null");
}

السؤال 5: كيف يتم تنفيذ اختبار عدة مرات مع معلمات مختلفة؟

الإجابة:

@ParameterizedTest
@ValueSource(strings = {"a@b.com", "test@domain.org"})
void shouldValidateEmail(String email) {
assertTrue(validator.isValidEmail(email));
}

@ParameterizedTest
@CsvSource({
"1, 2, 3",
"5, 5, 10",
"0, 0, 0"
})
void shouldAdd(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}

السؤال 6: كيف يتم تجميع الاختبارات؟

الإجابة:

@Nested
@DisplayName("When user is logged in")
class WhenLoggedIn {

@BeforeEach
void setup() {
login();
}

@Test
void canAccessProfile() { }

@Test
void canLogout() { }
}

@Nested
@DisplayName("When user is anonymous")
class WhenAnonymous {

@Test
void cannotAccessProfile() { }
}

السؤال 7: كيف يتم تعطيل اختبار بشكل شرطي؟

الإجابة:

@Test
@EnabledOnOs(OS.LINUX)
void onlyOnLinux() { }

@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "CI")
void onlyInCI() { }

@Test
@DisabledIf("isWeekend")
void notOnWeekend() { }

boolean isWeekend() {
return LocalDate.now().getDayOfWeek().getValue() > 5;
}

السؤال 8: كيف يتم إعطاء مهلة لاختبار؟

الإجابة:

@Test
@Timeout(value = 5, unit = TimeUnit.SECONDS)
void shouldCompleteInTime() {
// اختبار يجب أن ينتهي في أقل من 5 ثواني
}

// أو مع assertTimeout
@Test
void shouldCompleteInTime() {
assertTimeout(Duration.ofSeconds(2), () -> {
service.longOperation();
});
}

السؤال 9: كيف يتم إعادة استخدام التكوين بين الاختبارات؟

الإجابة:

// فئة أساسية
abstract class BaseTest {

@BeforeEach
void commonSetup() {
// تكوين مشترك
}
}

class UserServiceTest extends BaseTest {

@Test
void testUser() { }
}

// أو مع Extension
@ExtendWith(DatabaseExtension.class)
class DatabaseTest { }

السؤال 10: كيف يتم تنفيذ الاختبارات بترتيب معين؟

الإجابة:

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTest {

@Test
@Order(1)
void createUser() { }

@Test
@Order(2)
void updateUser() { }

@Test
@Order(3)
void deleteUser() { }
}

ملاحظة: تجنب الاختبارات المعتمدة إذا أمكن!


الأسئلة 11-20: Mockito​

السؤال 11: كيف يتم إنشاء mock باستخدام Mockito؟

الإجابة:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

@Mock
private UserRepository userRepository;

@InjectMocks
private UserService userService;

// أو يدوياً
UserRepository repo = mock(UserRepository.class);
}

السؤال 12: كيف يتم تحديد سلوك mock؟

الإجابة:

// إرجاع قيمة
when(userRepository.findById(1L)).thenReturn(Optional.of(user));

// إطلاق استثناء
when(userRepository.findById(999L)).thenThrow(new NotFoundException());

// استجابة ديناميكية
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User u = invocation.getArgument(0);
u.setId(1L);
return u;
});

السؤال 13: كيف يتم التحقق من أن طريقة قد تم استدعاؤها؟

الإجابة:

// التحقق من الاستدعاء
verify(userRepository).save(any(User.class));

// التحقق من عدد الاستدعاءات
verify(userRepository, times(2)).findById(anyLong());
verify(userRepository, never()).delete(any());
verify(userRepository, atLeast(1)).findAll();

السؤال 14: ما الفرق بين @Mock و @Spy؟

الإجابة:

  • @Mock: ينشئ كائن وهمي، جميع الطرق تعيد null/0/false
  • @Spy: يستخدم الكائن الحقيقي، يمكن تجاوز بعض الطرق
@Spy
private UserService userService = new UserService();

// فقط هذه الطريقة يتم محاكاتها
doReturn(user).when(userService).findById(1L);

// الطرق الأخرى تنفذ الكود الحقيقي

السؤال 15: كيف يتم التقاط المعاملات الممررة إلى mock؟

الإجابة:

@Captor
ArgumentCaptor<User> userCaptor;

@Test
void shouldCaptureUser() {
userService.createUser(request);

verify(userRepository).save(userCaptor.capture());

User savedUser = userCaptor.getValue();
assertThat(savedUser.getEmail()).isEqualTo("test@example.com");
}

السؤال 16: كيف يتم محاكاة طريقة void؟

الإجابة:

// لا تفعل شيئاً (افتراضي)
doNothing().when(emailService).sendEmail(any());

// إطلاق استثناء
doThrow(new MailException()).when(emailService).sendEmail(any());

// تنفيذ كود
doAnswer(invocation -> {
String email = invocation.getArgument(0);
log.info("Would send email to: {}", email);
return null;
}).when(emailService).sendEmail(any());

السؤال 17: كيف يتم محاكاة الطرق الثابتة؟

الإجابة:

@Test
void testStaticMethod() {
try (MockedStatic<UUID> mockedUUID = mockStatic(UUID.class)) {
UUID fakeUUID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
mockedUUID.when(UUID::randomUUID).thenReturn(fakeUUID);

// اختبار
String id = service.generateId();
assertThat(id).contains("123e4567");
}
}

السؤال 18: كيف يتم التحقق من ترتيب الاستدعاءات؟

الإجابة:

@Test
void shouldCallInOrder() {
service.process();

InOrder inOrder = inOrder(validator, repository, notifier);

inOrder.verify(validator).validate(any());
inOrder.verify(repository).save(any());
inOrder.verify(notifier).notify(any());
}

السؤال 19: كيف يتم استخدام matchers في Mockito؟

الإجابة:

when(repo.findByName(anyString())).thenReturn(user);
when(repo.findByAge(eq(25))).thenReturn(users);
when(repo.search(argThat(s -> s.length() > 3))).thenReturn(results);

verify(repo).save(argThat(user ->
user.getEmail().contains("@") && user.getAge() > 18
));

السؤال 20: كيف يتم إعادة تعيين mock بين الاختبارات؟

الإجابة:

@AfterEach
void resetMocks() {
reset(userRepository);
}

// أو لـ mock محدد
Mockito.clearInvocations(userRepository);

الأسئلة 21-30: اختبارات التكامل​

السؤال 21: ما هو التعليق التوضيحي لاختبار تكامل Spring Boot؟

الإجابة:

@SpringBootTest
class IntegrationTest {
// تحميل كل سياق Spring
}

// مع خادم ويب
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebIntegrationTest { }

السؤال 22: كيف يتم اختبار الطبقة الويب فقط؟

الإجابة:

@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void shouldReturnUser() throws Exception {
when(userService.findById(1L)).thenReturn(user);

mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"));
}
}

السؤال 23: كيف يتم اختبار الطبقة repository فقط؟

الإجابة:

@DataJpaTest
class UserRepositoryTest {

@Autowired
private TestEntityManager entityManager;

@Autowired
private UserRepository userRepository;

@Test
void shouldFindByEmail() {
User user = new User("test@example.com");
entityManager.persist(user);
entityManager.flush();

Optional<User> found = userRepository.findByEmail("test@example.com");

assertThat(found).isPresent();
}
}

السؤال 24: كيف يتم استخدام TestRestTemplate؟

الإجابة:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApiIntegrationTest {

@Autowired
private TestRestTemplate restTemplate;

@Test
void shouldCreateUser() {
UserRequest request = new UserRequest("test@example.com");

ResponseEntity<User> response = restTemplate.postForEntity(
"/api/users", request, User.class);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
}

السؤال 25: كيف يتم استخدام قاعدة بيانات اختبار؟

الإجابة:

// H2 في الذاكرة (تلقائي مع @DataJpaTest)

// أو Testcontainers
@Testcontainers
@SpringBootTest
class PostgresIntegrationTest {

@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15");

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}

السؤال 26: كيف يتم اختبار باستخدام ملف تعريف محدد؟

الإجابة:

@SpringBootTest
@ActiveProfiles("test")
class TestProfileTest {
// يستخدم application-test.yml
}
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb

السؤال 27: كيف يتم اختبار الأمان؟

الإجابة:

@WebMvcTest(SecuredController.class)
@Import(SecurityConfig.class)
class SecurityTest {

@Test
void anonymousCannotAccess() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isUnauthorized());
}

@Test
@WithMockUser(roles = "ADMIN")
void adminCanAccess() throws Exception {
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk());
}
}

السؤال 28: كيف يتم اختبار التحقق؟

الإجابة:

@Test
void shouldRejectInvalidEmail() throws Exception {
String invalidRequest = """
{"email": "not-an-email", "name": "John"}
""";

mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidRequest))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors.email").exists());
}

السؤال 29: كيف يتم قياس تغطية الكود؟

الإجابة: باستخدام JaCoCo:

<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>

التقرير في target/site/jacoco/index.html


السؤال 30: ما هي أفضل ممارسات الاختبار؟

الإجابة:

  1. اختبار واحد = سلوك واحد: لا توجد تأكيدات متعددة معقدة
  2. اختبارات مستقلة: لا توجد تبعيات بين الاختبارات
  3. أسماء وصفية: shouldReturnUserWhenValidId
  4. بيانات معزولة: كل اختبار يجهز بياناته
  5. محاكاة التبعيات الخارجية: API، البريد الإلكتروني، إلخ.
  6. تجنب الاختبارات غير المستقرة: لا توجد تبعيات على الوقت
  7. اختبار حالات الخطأ: ليس فقط المسار السعيد
  8. تغطية معقولة: استهدف 70-80%، وليس 100%

النتيجة​

النتيجةالمستوى
27-30ممتاز - إتقان الاختبارات
21-26جيد - بعض المراجعات
15-20متوسط - أعد قراءة الفصول
أقل من 15بحاجة إلى مراجعة

العودة إلى الوحدة 9