404 Not Found

404 Not Found


nginx

Testing Strategy

Tests are a guarantee of quality—writing code without tests is like driving without a seatbelt; sooner or later, an accident will happen.

1. What You'll Learn


2. A True Story of a Quality Engineer

(1) Pain Point: Fixing one bug leads to three more

Alice modified the order cancellation logic, thinking it would only affect the cancellation feature, but it ended up causing issues with inventory restoration as well, which in turn affected the reporting statistics. Every time she changes the code, she's on edge because there are no automated tests to verify regression. Bob is frequently woken up in the middle of the night by calls for emergency fixes, and the team spends 30% of their time each week fixing bugs.

(2) Solutions for Automated Testing

Write good tests to provide a "safety net" when modifying code:

JAVA
@SpringBootTest
class OrderServiceTest {

    @Test
    void shouldCancelOrderAndRestoreStock() {
        // Given: product with stock 10, order with quantity 3
        // When: cancel order
        // Then: stock restored to 13, order status is CANCELLED
    }
}

(3) Revenue

Alice wrote 50 test cases for the OrderFlow core process, achieving 85% code coverage. Every time the code is modified, the tests run automatically, and regression issues are detected within five minutes—so Bob no longer has to fix bugs in the middle of the night.


3. The Testing Pyramid

(1) Three-Tier Testing Strategy

100%
graph LR
    A["Unit Tests<br/>70% - Fast, Isolated<br/>JUnit + Mockito"] --> B["Integration Tests<br/>20% - Medium Speed<br/>@SpringBootTest + H2"]
    B --> C["E2E Tests<br/>10% - Slow, Full Stack<br/>TestContainers + REST Assured"]
Level Percentage Speed Dependencies Test Content
Unit Testing 70% Millisecond-level Mock Business Logic
Integration Testing 20% Seconds H2/TestContainers Component Interaction
E2E Testing 10% Minutes Full Environment Business Process

(1) ▶ Example: Unit Testing

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());
    }
}

Output:

TEXT
// Execution Successful

4. @WebMvcTest Slicing Tests

(1) Controller-Level Testing

(1) ▶ Example: Controller Slicing Test

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"));
    }
}

Output:

TEXT
// Execution Successful
Test Annotation Loading Scope Applicable Level
@WebMvcTest Controller + Web Layer Controller
@DataJpaTest Repository + JPA Layer Repository
@SpringBootTest Full ApplicationContext Integration Testing

5. @DataJpaTest Data Layer Testing

(1) ▶ Example: Repository Testing

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);
    }
}

Output:

TEXT
// Execution Successful

6. TestContainers: Containerized Integration Testing

(1) Live Database Testing

(1) ▶ Example: 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>

Output:

TEXT
// Execution Successful
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) ▶ Example: 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));

        // First call: database query
        productService.getProduct(product.getId());
        // Second call: Redis cache hit
        Product cached = productService.getProduct(product.getId());

        assertThat(cached).isNotNull();
    }
}

Output:

TEXT
// Execution Successful

7. Test Coverage

(1) ▶ Example: JaCoCo Configuration

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>

Output:

TEXT
// Execution Successful
Coverage Dimension Objective Description
Line Coverage ≥ 80% Core Business Code
Branch Coverage ≥ 70% if/else Logic
Method Coverage ≥ 80% Public Methods

8. Comprehensive Example: Testing the Core Order Placement Process in OrderFlow

JAVA
// OrderServiceTest.java (Unit Test)
@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 (Slice Test)
@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());
    }
}

❓ FAQ

Q What is the difference between @MockBean and @Mock?
A @Mock is a Mockito annotation used to create pure mock objects; it does not depend on Spring. @MockBean is a Spring Boot annotation that injects mock objects into the Spring ApplicationContext to replace beans in the container. Use @Mock for unit tests and @MockBean for integration tests.
Q Which should I choose: @SpringBootTest or @WebMvcTest?
A @WebMvcTest loads only the Web layer (Controller + Filter), which is faster and suitable for Controller unit tests. @SpringBootTest loads the full ApplicationContext and is suitable for end-to-end integration tests.
Q Will TestContainers slow down testing?
A The first time a container is started, the image must be downloaded; however, containers are shared across test classes (via the static field), so subsequent tests reuse them directly. Typical overhead: 30 seconds for the first run, and less than 5 seconds for subsequent runs. We recommend prewarming images in the CI environment.
Q Do H2 and MySQL behave the same way?
A They are mostly consistent, but there are some differences: 1) MySQL's AUTO_INCREMENT vs. H2's IDENTITY; 2) MySQL-specific functions (GROUP_CONCAT); 3) Different default transaction isolation levels. For production environments, we recommend using TestContainers + MySQL.
Q How do I test @Async methods?
A 1) Test the asynchronous method directly (without using a proxy); 2) Use CompletableFuture.get() to wait for the result; 3) Configure a synchronous executor (SyncTaskExecutor) to simplify testing.
Q Is 80% test coverage sufficient?
A 80% is the minimum standard for core business code; we recommend 85% or higher. Coverage is only a reference; what matters most is test quality—covering boundary conditions, exception paths, and concurrent scenarios, not just normal flow paths.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Write five unit tests for OrderService that cover scenarios such as creating an order, insufficient inventory, canceling an order, status exceptions, and resources not found.

  2. Advanced Exercise (Difficulty ⭐⭐): Use @WebMvcTest to write slice tests for the OrderController, covering scenarios such as normal requests, validation failures, 404 errors, and 401 (Unauthorized) errors. Use @DataJpaTest to write query tests for the ProductRepository.

  3. Challenge (Difficulty: ⭐⭐⭐): Integrate TestContainers (MySQL + Redis) and write an end-to-end integration test—covering the entire process from creating a product, placing an order and deducting inventory, Redis cache hits, to canceling an order and restoring inventory, with a coverage rate of ≥ 80%.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏