Testing trong Java

1. Nguyên Tắc Testing

1.1 Tại Sao Cần Testing?

  • Đảm bảo chất lượng code
  • Phát hiện lỗi sớm
  • Tài liệu hóa hành vi
  • Thiết kế tốt hơn
  • Tự tin khi refactor

1.2 Các Loại Testing

  • Unit Testing
  • Integration Testing
  • System Testing
  • Performance Testing
  • Security Testing
  • End-to-End Testing

2. Unit Testing

2.1 JUnit 5 Basics

@DisplayName("Order Service Tests")
class OrderServiceTest {
    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OrderService orderService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    @DisplayName("Should create order successfully")
    void whenCreateOrder_thenSuccess() {
        // Given
        OrderRequest request = createValidOrderRequest();
        Order order = new Order();
        when(orderRepository.save(any(Order.class)))
            .thenReturn(order);

        // When
        OrderResponse response = orderService.createOrder(request);

        // Then
        assertNotNull(response);
        verify(orderRepository).save(any(Order.class));
        verify(paymentService).processPayment(any(Order.class));
    }

    @Test
    @DisplayName("Should throw exception for invalid order")
    void whenCreateInvalidOrder_thenThrowException() {
        // Given
        OrderRequest request = createInvalidOrderRequest();

        // When/Then
        assertThrows(ValidationException.class, () -> 
            orderService.createOrder(request));
    }

    @ParameterizedTest
    @ValueSource(strings = {"NEW", "PROCESSING", "COMPLETED"})
    void whenUpdateStatus_thenSuccess(String status) {
        // Test implementation
    }
}

2.2 Mockito Advanced

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Captor
    private ArgumentCaptor<Order> orderCaptor;

    @Test
    void shouldCaptureOrderDetails() {
        // Given
        OrderRequest request = createOrderRequest();

        // When
        orderService.createOrder(request);

        // Then
        verify(orderRepository).save(orderCaptor.capture());
        Order savedOrder = orderCaptor.getValue();
        assertEquals(request.getCustomerId(), 
            savedOrder.getCustomerId());
    }

    @Test
    void shouldHandleException() {
        // Given
        when(orderRepository.save(any()))
            .thenThrow(new RuntimeException("DB Error"));

        // When/Then
        assertThrows(ServiceException.class, () ->
            orderService.createOrder(createOrderRequest()));
    }

    @Test
    void shouldUseSpyObject() {
        // Given
        OrderValidator validator = spy(new OrderValidator());
        doReturn(true).when(validator)
            .isValidCustomer(anyString());

        // When
        boolean result = validator
            .validateOrder(createOrderRequest());

        // Then
        assertTrue(result);
        verify(validator).isValidCustomer(anyString());
    }
}

3. Integration Testing

3.1 Spring Boot Test

@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerIntegrationTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private PaymentService paymentService;

    @Test
    void whenCreateOrder_thenSuccess() throws Exception {
        // Given
        OrderRequest request = createOrderRequest();
        String requestJson = objectMapper
            .writeValueAsString(request);

        // When/Then
        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestJson))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.orderId").exists())
            .andExpect(jsonPath("$.status").value("NEW"));
    }

    @Test
    void whenGetOrder_thenSuccess() throws Exception {
        // Given
        String orderId = "123";

        // When/Then
        mockMvc.perform(get("/api/v1/orders/{id}", orderId)
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.orderId").value(orderId))
            .andDo(print());
    }
}

3.2 Database Testing

@DataJpaTest
@AutoConfigureTestDatabase(replace = 
    AutoConfigureTestDatabase.Replace.NONE)
class OrderRepositoryTest {
    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void whenSaveOrder_thenSuccess() {
        // Given
        Order order = createOrder();

        // When
        Order savedOrder = orderRepository.save(order);

        // Then
        assertNotNull(savedOrder.getId());
        assertEquals(order.getCustomerId(), 
            savedOrder.getCustomerId());
    }

    @Test
    void whenFindByStatus_thenSuccess() {
        // Given
        Order order1 = createOrder("NEW");
        Order order2 = createOrder("NEW");
        Order order3 = createOrder("PROCESSING");
        entityManager.persist(order1);
        entityManager.persist(order2);
        entityManager.persist(order3);

        // When
        List<Order> newOrders = 
            orderRepository.findByStatus("NEW");

        // Then
        assertEquals(2, newOrders.size());
    }
}

4. Performance Testing

4.1 JMeter Test Plan

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
    <hashTree>
        <TestPlan guiclass="TestPlanGui" 
                  testclass="TestPlan" 
                  testname="Order API Test Plan">
            <elementProp name="TestPlan.user_defined_variables"
                        elementType="Arguments">
                <collectionProp name="Arguments.arguments"/>
            </elementProp>
            <boolProp name="TestPlan.functional_mode">false</boolProp>
            <stringProp name="TestPlan.comments"></stringProp>
            <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
        </TestPlan>
        <hashTree>
            <ThreadGroup guiclass="ThreadGroupGui" 
                        testclass="ThreadGroup"
                        testname="Order Creation">
                <elementProp name="ThreadGroup.main_controller"
                            elementType="LoopController">
                    <boolProp name="LoopController.continue_forever">false</boolProp>
                    <intProp name="LoopController.loops">100</intProp>
                </elementProp>
                <stringProp name="ThreadGroup.num_threads">10</stringProp>
                <stringProp name="ThreadGroup.ramp_time">1</stringProp>
                <longProp name="ThreadGroup.start_time">1</longProp>
                <longProp name="ThreadGroup.end_time">1</longProp>
                <boolProp name="ThreadGroup.scheduler">false</boolProp>
                <stringProp name="ThreadGroup.duration"></stringProp>
                <stringProp name="ThreadGroup.delay"></stringProp>
            </ThreadGroup>
        </hashTree>
    </hashTree>
</jmeterTestPlan>

4.2 Gatling Test

class OrderSimulation extends Simulation {
    val httpProtocol = http
        .baseUrl("http://localhost:8080")
        .acceptHeader("application/json")

    val scn = scenario("Create Order Scenario")
        .exec(http("Create Order Request")
            .post("/api/v1/orders")
            .body(StringBody("""
                {
                    "customerId": "123",
                    "items": [
                        {
                            "productId": "456",
                            "quantity": 1
                        }
                    ]
                }"""))
            .asJson
            .check(status.is(201))
            .check(jsonPath("$.orderId").saveAs("orderId")))
        .pause(1)
        .exec(http("Get Order Request")
            .get("/api/v1/orders/${orderId}")
            .check(status.is(200)))

    setUp(
        scn.inject(
            rampUsers(100).during(10.seconds),
            constantUsersPerSec(10).during(20.seconds)
        )
    ).protocols(httpProtocol)
}

5. Security Testing

5.1 OWASP ZAP

@SpringBootTest(webEnvironment = DEFINED_PORT)
class SecurityTest {
    private static final String ZAP_PROXY_HOST = "localhost";
    private static final int ZAP_PROXY_PORT = 8080;
    private static final String TARGET_URL = 
        "http://localhost:8080";

    @Test
    void runZapScan() throws Exception {
        // Initialize ZAP Client
        ClientApi api = new ClientApi(ZAP_PROXY_HOST, ZAP_PROXY_PORT);

        // Start Spider
        api.spider.scan(TARGET_URL, null, null, null, null);

        // Wait for Spider completion
        int progress;
        do {
            Thread.sleep(1000);
            progress = Integer.parseInt(
                ((ApiResponseElement) api.spider.status())
                .getValue());
        } while (progress < 100);

        // Start Active Scan
        api.ascan.scan(TARGET_URL, "True", "False", null, null, null);

        // Wait for Active Scan completion
        do {
            Thread.sleep(1000);
            progress = Integer.parseInt(
                ((ApiResponseElement) api.ascan.status())
                .getValue());
        } while (progress < 100);

        // Generate Report
        byte[] report = api.core.htmlreport();
        FileUtils.writeByteArrayToFile(
            new File("zap-report.html"), report);
    }
}

5.2 Security Unit Tests

@SpringBootTest
class SecurityTest {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    void whenEncodePassword_thenSuccess() {
        // Given
        String rawPassword = "MyPassword123!";

        // When
        String encodedPassword = 
            passwordEncoder.encode(rawPassword);

        // Then
        assertTrue(passwordEncoder.matches(
            rawPassword, encodedPassword));
        assertNotEquals(rawPassword, encodedPassword);
    }

    @Test
    void whenValidateJwt_thenSuccess() {
        // Given
        String token = generateValidJwt();

        // When
        Claims claims = Jwts.parser()
            .setSigningKey(SECRET_KEY)
            .parseClaimsJws(token)
            .getBody();

        // Then
        assertNotNull(claims.getSubject());
        assertTrue(claims.getExpiration()
            .after(new Date()));
    }
}

6. End-to-End Testing

6.1 Selenium WebDriver

@SpringBootTest(webEnvironment = DEFINED_PORT)
class OrderE2ETest {
    private WebDriver driver;

    @BeforeEach
    void setUp() {
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--headless");
        driver = new ChromeDriver(options);
    }

    @Test
    void shouldCreateOrder() {
        // Navigate to order page
        driver.get("http://localhost:8080/orders/new");

        // Fill order form
        driver.findElement(By.id("customerId"))
            .sendKeys("123");
        driver.findElement(By.id("productId"))
            .sendKeys("456");
        driver.findElement(By.id("quantity"))
            .sendKeys("1");

        // Submit form
        driver.findElement(By.id("submit-button"))
            .click();

        // Verify success
        WebElement successMessage = new WebDriverWait(driver, 10)
            .until(ExpectedConditions.presenceOfElementLocated(
                By.className("success-message")));

        assertTrue(successMessage.isDisplayed());
        assertTrue(successMessage.getText()
            .contains("Order created successfully"));
    }

    @AfterEach
    void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
}

6.2 Cucumber BDD

// OrderSteps.java
@SpringBootTest
public class OrderSteps {
    @Given("a valid customer with id {string}")
    public void givenValidCustomer(String customerId) {
        // Setup customer
    }

    @When("customer creates an order with product {string}")
    public void whenCreateOrder(String productId) {
        // Create order
    }

    @Then("order should be created successfully")
    public void thenOrderCreated() {
        // Verify order
    }
}

// orders.feature
Feature: Order Management

  Scenario: Create new order
    Given a valid customer with id "123"
    When customer creates an order with product "456"
    Then order should be created successfully

  Scenario Outline: Order validation
    Given a customer with id "<customerId>"
    When customer creates an order with product "<productId>"
    Then system should return "<result>"

    Examples:
      | customerId | productId | result  |
      | 123       | 456       | success |
      | invalid   | 456       | error   |
      | 123       | invalid   | error   |

7. Test Coverage và Quality

7.1 JaCoCo Configuration

<!-- pom.xml -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>${jacoco.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>check</id>
            <goals>
                <goal>check</goal>
            </goals>
            <configuration>
                <rules>
                    <rule>
                        <element>PACKAGE</element>
                        <limits>
                            <limit>
                                <counter>LINE</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

7.2 SonarQube Integration

<!-- pom.xml -->
<properties>
    <sonar.host.url>http://localhost:9000</sonar.host.url>
    <sonar.login>${SONAR_TOKEN}</sonar.login>
    <sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
    <sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
    <sonar.jacoco.reportPath>
        ${project.basedir}/target/jacoco.exec
    </sonar.jacoco.reportPath>
    <sonar.language>java</sonar.language>
</properties>

<plugin>
    <groupId>org.sonarsource.scanner.maven</groupId>
    <artifactId>sonar-maven-plugin</artifactId>
    <version>${sonar.version}</version>
</plugin>

8. Testing Best Practices

8.1 Test Structure

class OrderServiceTest {
    /**
     * Test structure follows AAA pattern:
     * - Arrange: Setup test data and conditions
     * - Act: Execute the test
     * - Assert: Verify the results
     */
    @Test
    void testOrderCreation() {
        // Arrange
        OrderRequest request = OrderRequest.builder()
            .customerId("123")
            .items(Arrays.asList(
                new OrderItem("456", 1)))
            .build();

        when(orderRepository.save(any()))
            .thenReturn(new Order());

        // Act
        OrderResponse response = 
            orderService.createOrder(request);

        // Assert
        assertNotNull(response);
        verify(orderRepository).save(any());
    }

    /**
     * Test naming convention:
     * whenX_thenY
     * givenX_whenY_thenZ
     */
    @Test
    void whenInvalidCustomer_thenThrowException() {
        // Test implementation
    }

    @Test
    void givenExistingOrder_whenCancel_thenStatusUpdated() {
        // Test implementation
    }
}

8.2 Test Data Management

@TestConfiguration
class TestConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("schema.sql")
            .addScript("test-data.sql")
            .build();
    }
}

class TestDataBuilder {
    public static Order createOrder() {
        return Order.builder()
            .customerId("123")
            .status(OrderStatus.NEW)
            .items(createOrderItems())
            .build();
    }

    public static List<OrderItem> createOrderItems() {
        return Arrays.asList(
            OrderItem.builder()
                .productId("456")
                .quantity(1)
                .price(BigDecimal.TEN)
                .build()
        );
    }
}

9. References và Further Reading

9.1 Books

  • Test Driven Development (Kent Beck)
  • Effective Unit Testing (Lasse Koskela)
  • Continuous Delivery (Jez Humble)
  • Clean Code (Robert C. Martin)
  • Working Effectively with Legacy Code (Michael Feathers)

9.2 Online Resources