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