404 Not Found

404 Not Found


nginx

استراتيجية الاختبار

الاختبارات هي ضمان الجودة—كتابة كود بدون اختبارات كالقيادة بدون حزام أمان؛ عاجلاً أم آجلاً سيقع حادث.

1. ما ستتعلمه


2. قصة حقيقية لمهندس جودة

(1) نقطة الألم: إصلاح خلل يؤدي إلى ثلاثة أخرى

عدّلت Alice منطق إلغاء الطلبات، معتقدة أنه سيؤثر فقط على ميزة الإلغاء، لكنه تسبب في مشكلات في استعادة المخزون أيضًا، مما أثر بدوره على إحصائيات التقارير. في كل مرة تغير الكود، تكون في توتر لأنه لا توجد اختبارات آلية للتحقق من الانحدار. Bob يُستيقظ كثيرًا في منتصف الليل بمكالمات للإصلاحات الطارئة، والفريق يقضي 30% من وقته أسبوعيًا في إصلاح الأخطاء.

(2) حلول الاختبار الآلي

اكتب اختبارات جيدة لتوفير "شبكة أمان" عند تعديل الكود:

JAVA
@SpringBootTest
class OrderServiceTest {

    @Test
    void shouldCancelOrderAndRestoreStock() {
        // Given: منتج بمخزون 10، طلب بكمية 3
        // When: إلغاء الطلب
        // Then: المخزون يعود إلى 13، حالة الطلب CANCELLED
    }
}

(3) العائد

كتبت Alice 50 حالة اختبار لعملية OrderFlow الأساسية، محققةً 85% تغطية كود. في كل مرة يُعدل الكود، تعمل الاختبارات تلقائيًا، وتُكتشف مشكلات الانحدار خلال خمس دقائق—لذا لم يعد Bob يضطر لإصلاح الأخطاء في منتصف الليل.


3. هرم الاختبار

(1) استراتيجية الاختبار ثلاثية المستويات

100%
graph LR
    A["اختبارات الوحدة<br/>70% - سريعة، معزولة<br/>JUnit + Mockito"] --> B["اختبارات التكامل<br/>20% - سرعة متوسطة<br/>@SpringBootTest + H2"]
    B --> C["اختبارات E2E<br/>10% - بطيئة، مكدس كامل<br/>TestContainers + REST Assured"]
المستوى النسبة السرعة الاعتماديات محتوى الاختبار
اختبار الوحدة 70% مستوى المللي ثانية Mock المنطق التجاري
اختبار التكامل 20% ثوانٍ H2/TestContainers تفاعل المكونات
اختبار E2E 10% دقائق البيئة الكاملة العملية التجارية

(1) ▶ مثال:اختبار الوحدة

JAVA
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private ProductRepository productRepository;

    @InjectMocks
    private OrderServiceImpl orderService;

    @Test
    void shouldCreateOrderSuccessfully() {
        // Given
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 10);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));

        // When
        Order order = orderService.createOrder(1L, 3);

        // Then
        assertThat(order).isNotNull();
        assertThat(product.getStock()).isEqualTo(7);
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    void shouldThrowWhenInsufficientStock() {
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 2);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));

        assertThatThrownBy(() -> orderService.createOrder(1L, 5))
            .isInstanceOf(InsufficientStockException.class);

        verify(orderRepository, never()).save(any());
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

4. اختبارات شرائح @WebMvcTest

(1) اختبار مستوى المتحكم

(1) ▶ مثال:اختبار شرائح المتحكم

JAVA
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Test
    void shouldReturnOrderById() throws Exception {
        OrderResponse order = new OrderResponse(
            1L, "bob", "PENDING", BigDecimal.valueOf(999),
            List.of(), Instant.now());

        when(orderService.getOrder(1L)).thenReturn(order);

        mockMvc.perform(get("/api/v1/orders/1")
                .with(httpBasic("bob", "pass123")))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.status").value("PENDING"));
    }

    @Test
    void shouldReturn404WhenOrderNotFound() throws Exception {
        when(orderService.getOrder(999L))
            .thenThrow(new ResourceNotFoundException("Order", 999L));

        mockMvc.perform(get("/api/v1/orders/999")
                .with(httpBasic("bob", "pass123")))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.code").value("RESOURCE_NOT_FOUND"));
    }

    @Test
    void shouldReturn400WhenValidationFails() throws Exception {
        String body = """
            {"productId": null, "quantity": 0}
            """;

        mockMvc.perform(post("/api/v1/orders")
                .with(httpBasic("bob", "pass123"))
                .contentType(MediaType.APPLICATION_JSON)
                .content(body))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح
تعليق الاختبار نطاق التحميل المستوى المطبق
@WebMvcTest المتحكم + طبقة الويب المتحكم
@DataJpaTest المستودع + طبقة JPA المستودع
@SpringBootTest سياق التطبيق الكامل اختبار التكامل

5. اختبار طبقة البيانات @DataJpaTest

(1) ▶ مثال:اختبار المستودع

JAVA
@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldFindByNameContaining() {
        entityManager.persist(new Product("Laptop Pro", BigDecimal.valueOf(1299), 50));
        entityManager.persist(new Product("Laptop Air", BigDecimal.valueOf(899), 30));
        entityManager.persist(new Product("Wireless Mouse", BigDecimal.valueOf(29), 200));

        List<Product> results = productRepository.findByNameContaining("Laptop");

        assertThat(results).hasSize(2);
    }

    @Test
    void shouldFindOutOfStockProducts() {
        Product outOfStock = new Product("Old Model", BigDecimal.valueOf(99), 0);
        entityManager.persist(outOfStock);
        entityManager.persist(new Product("New Model", BigDecimal.valueOf(199), 10));

        List<Product> results = productRepository.findOutOfStockProducts();

        assertThat(results).hasSize(1);
        assertThat(results.get(0).getName()).isEqualTo("Old Model");
    }

    @Test
    void shouldCheckStockBeforeDeduction() {
        Product product = entityManager.persist(
            new Product("Laptop", BigDecimal.valueOf(999), 5));

        Product found = productRepository.findById(product.getId()).orElseThrow();
        found.deductStock(3);

        Product updated = productRepository.findById(product.getId()).orElseThrow();
        assertThat(updated.getStock()).isEqualTo(2);
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

6. TestContainers: اختبار تكوين حاوي

(1) اختبار قاعدة بيانات حية

(1) ▶ مثال:MySQL TestContainer

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

الناتج:

TEXT
// تم التنفيذ بنجاح
JAVA
@SpringBootTest
@Testcontainers
class OrderServiceIntegrationTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("orderflow_test")
        .withUsername("test")
        .withPassword("test");

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

    @Autowired
    private OrderService orderService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    void shouldCreateOrderAndDeductStock() {
        Product product = productRepository.save(
            new Product("Laptop", BigDecimal.valueOf(999), 10));

        Order order = orderService.createOrder(product.getId(), 3);

        assertThat(order.getStatus()).isEqualTo("PENDING");

        Product updated = productRepository.findById(product.getId()).orElseThrow();
        assertThat(updated.getStock()).isEqualTo(7);
    }
}

(2) ▶ مثال:Redis TestContainer

JAVA
@SpringBootTest
@Testcontainers
class RedisCacheIntegrationTest {

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

    @DynamicPropertySource
    static void configureRedis(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
    }

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    void shouldCacheProductInRedis() {
        Product product = productRepository.save(
            new Product("Laptop", BigDecimal.valueOf(999), 10));

        // الاستدعاء الأول: استعلام قاعدة البيانات
        productService.getProduct(product.getId());
        // الاستدعاء الثاني: إصابة تخزين Redis المؤقت
        Product cached = productService.getProduct(product.getId());

        assertThat(cached).isNotNull();
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

7. تغطية الاختبار

(1) ▶ مثال:تكوين JaCoCo

XML
<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>

الناتج:

TEXT
// تم التنفيذ بنجاح
بُعد التغطية الهدف الوصف
تغطية السطور ≥ 80% كود الأعمال الأساسي
تغطية الفروع ≥ 70% منطق if/else
تغطية الطرق ≥ 80% الطرق العامة

8. مثال شامل: اختبار عملية تقديم الطلبات الأساسية في OrderFlow

JAVA
// OrderServiceTest.java (اختبار وحدة)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock private OrderRepository orderRepository;
    @Mock private ProductRepository productRepository;
    @InjectMocks private OrderServiceImpl orderService;

    @Test
    void createOrder_success() {
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 10);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));
        when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Order order = orderService.createOrder(1L, 3);

        assertThat(order.getStatus()).isEqualTo("PENDING");
        assertThat(product.getStock()).isEqualTo(7);
    }

    @Test
    void createOrder_insufficientStock_throws() {
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 2);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));

        assertThatThrownBy(() -> orderService.createOrder(1L, 5))
            .isInstanceOf(InsufficientStockException.class);
    }

    @Test
    void cancelOrder_success_restoresStock() {
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 7);
        Order order = new Order();
        order.setStatus("PENDING");
        order.addItem(product, 3);
        when(orderRepository.findByIdWithItems(1L)).thenReturn(Optional.of(order));

        orderService.cancelOrder(1L);

        assertThat(order.getStatus()).isEqualTo("CANCELLED");
        assertThat(product.getStock()).isEqualTo(10);
    }

    @Test
    void cancelOrder_nonPending_throws() {
        Order order = new Order();
        order.setStatus("SHIPPED");
        when(orderRepository.findByIdWithItems(1L)).thenReturn(Optional.of(order));

        assertThatThrownBy(() -> orderService.cancelOrder(1L))
            .isInstanceOf(OrderStateException.class);
    }
}

// OrderControllerTest.java (اختبار شرائح)
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired MockMvc mockMvc;
    @MockBean OrderService orderService;

    @Test
    @WithMockUser(roles = "CUSTOMER")
    void createOrder_validRequest_returns201() throws Exception {
        when(orderService.createOrder(any(), eq("bob"))).thenReturn(
            new OrderResponse(1L, "bob", "PENDING", BigDecimal.valueOf(999), List.of(), Instant.now()));

        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"productId\":1,\"quantity\":3}"))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.status").value("PENDING"));
    }

    @Test
    void createOrder_unauthenticated_returns401() throws Exception {
        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"productId\":1,\"quantity\":3}"))
            .andExpect(status().isUnauthorized());
    }
}

❓ أسئلة شائعة

س ما الفرق بين @MockBean و@Mock؟
ج @Mock هو تعليق Mockito يُستخدم لإنشاء كائنات محاكاة نقية؛ لا يعتمد على Spring. @MockBean هو تعليق Spring Boot يحقن كائنات محاكاة في سياق تطبيق Spring لاستبدال beans في الحاوية. استخدم @Mock لاختبارات الوحدة و@MockBean لاختبارات التكامل.
س أيهما أختار: @SpringBootTest أم @WebMvcTest؟
ج @WebMvcTest يحمل طبقة الويب فقط (المتحكم + الفلتر)، وهو أسرع وأكثر ملاءمة لاختبارات وحدة المتحكم. @SpringBootTest يحمل سياق التطبيق الكامل وهو مناسب لاختبارات التكامل الشاملة.
س هل ستبطئ TestContainers الاختبارات؟
ج في أول مرة يُبدأ فيها الحاوية، يجب تنزيل الصورة؛ ومع ذلك، تُشارك الحاويات بين فئات الاختبار (عبر الحقل static)، لذا الاختبارات اللاحقة تعيد استخدامها مباشرة. العبء النموذجي: 30 ثانية لأول تشغيل، وأقل من 5 ثوانٍ للتشغيلات اللاحقة. نوصي بتسخين الصور مسبقًا في بيئة CI.
س هل H2 وMySQL يتصرفان بنفس الطريقة؟
ج متسقان في الغالب، لكن هناك بعض الاختلافات: 1) AUTO_INCREMENT في MySQL مقابل IDENTITY في H2؛ 2) دوال MySQL الخاصة (GROUP_CONCAT)؛ 3) مستويات عزل المعاملات الافتراضية مختلفة. لبيئات الإنتاج، نوصي باستخدام TestContainers + MySQL.
س كيف أختبر طرق @Async؟
ج 1) اختبر الطريقة غير المتزامنة مباشرة (بدون وكيل)؛ 2) استخدم CompletableFuture.get() لانتظار النتيجة؛ 3) كوّن منفذًا متزامنًا (SyncTaskExecutor) لتبسيط الاختبار.
س هل تغطية اختبار 80% كافية؟
ج 80% هي الحد الأدنى لكود الأعمال الأساسية؛ نوصي بـ 85% أو أعلى. التغطية هي مرجع فقط؛ الأهم هو جودة الاختبار—تغطية الشروط الحدية ومسارات الاستثناء والسيناريوهات المتزامنة، وليس فقط مسارات التدفق الطبيعي.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (صعوبة: ⭐): اكتب خمسة اختبارات وحدة لـ OrderService تغطي سيناريوهات مثل إنشاء طلب، مخزون غير كافٍ، إلغاء طلب، استثناءات الحالة، وموارد غير موجودة.

  2. تمرين متقدم (صعوبة ⭐⭐): استخدم @WebMvcTest لكتابة اختبارات شرائح لـ OrderController، تغطي سيناريوهات مثل الطلبات العادية، فشل التحقق، أخطاء 404، وأخطاء 401 (غير مصرح). استخدم @DataJpaTest لكتابة اختبارات استعلامات لـ ProductRepository.

  3. تحدي (صعوبة: ⭐⭐⭐): ادمج TestContainers (MySQL + Redis) واكتب اختبار تكامل شامل—يغطي العملية الكاملة من إنشاء منتج، تقديم طلب وخصم مخزون، إصابة تخزين Redis المؤقت، إلى إلغاء طلب واستعادة مخزون، بمعدل تغطية ≥ 80%.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%