簡介
測驗是每個專業開發人員的必備技能。 Spring Boot 4 使用 JUnit Jupiter 6 並整合 Mockito 和 AssertJ 來輕鬆可靠地編寫單元測試。
1. 測試金字塔
╱ E2E Tests ╲ ← Ít nhất, chậm nhất
╱ Integration ╲
╱ Tests ╲
╱ Unit Tests ╲ ← Nhiều nhất, nhanh nhất
╱──────────────────────╲
- 單元測試:測試 1 個類別/方法隔離,模擬依賴關係
- 整合測試:一起測試多個元件,真實的資料庫
- E2E 測試:測試從 HTTP 請求 → 回應的完整流程
2.JUnit Jupiter 6 基礎知識
2.1 主要註釋
class ProductServiceTest {
@BeforeAll
static void setUpAll() { /* Chạy 1 lần trước tất cả tests */ }
@BeforeEach
void setUp() { /* Chạy trước MỖI test */ }
@Test
@DisplayName("Should create product successfully")
void shouldCreateProduct() { }
@Test
@Disabled("Not implemented yet")
void futureFeature() { }
@AfterEach
void tearDown() { /* Chạy sau MỖI test */ }
@AfterAll
static void tearDownAll() { /* Chạy 1 lần sau tất cả tests */ }
}
2.2 參數化測試
@ParameterizedTest
@ValueSource(strings = {"", " ", " "})
@DisplayName("Should reject blank product name")
void shouldRejectBlankName(String name) {
var request = new CreateProductRequest(name, BigDecimal.TEN, "Category");
assertThrows(ConstraintViolationException.class,
() -> productService.create(request));
}
@ParameterizedTest
@CsvSource({
"100, 10, 90",
"200, 50, 150",
"0, 0, 0"
})
void shouldCalculateDiscount(int price, int discount, int expected) {
assertEquals(expected, calculator.applyDiscount(price, discount));
}
3. Mockito — 模擬依賴項
3.1 使用@ExtendWith 設定
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository productRepository;
@Mock
private CategoryRepository categoryRepository;
@InjectMocks
private ProductService productService;
}
3.2 存根 — 定義模擬行為
@Test
@DisplayName("Should return product when found by ID")
void shouldReturnProductWhenFound() {
// Given
Long productId = 1L;
Product product = Product.builder()
.id(productId)
.name("Spring Boot Book")
.price(new BigDecimal("450000"))
.build();
when(productRepository.findById(productId))
.thenReturn(Optional.of(product));
// When
ProductResponse result = productService.getProduct(productId);
// Then
assertThat(result.id()).isEqualTo(productId);
assertThat(result.name()).isEqualTo("Spring Boot Book");
assertThat(result.price()).isEqualByComparingTo("450000");
}
3.3 驗證-檢查模擬是否被正確調用
@Test
@DisplayName("Should delete product and evict cache")
void shouldDeleteProduct() {
// Given
Long productId = 1L;
when(productRepository.existsById(productId)).thenReturn(true);
// When
productService.deleteProduct(productId);
// Then
verify(productRepository).deleteById(productId);
verify(productRepository, times(1)).existsById(productId);
verify(productRepository, never()).findById(any());
}
3.4 參數捕獲
@Test
void shouldSaveProductWithCorrectData() {
// Given
var request = new CreateProductRequest("Book", new BigDecimal("100"), "Education");
ArgumentCaptor<Product> productCaptor = ArgumentCaptor.forClass(Product.class);
when(productRepository.save(any(Product.class)))
.thenAnswer(inv -> {
Product p = inv.getArgument(0);
p.setId(1L);
return p;
});
// When
productService.create(request);
// Then
verify(productRepository).save(productCaptor.capture());
Product savedProduct = productCaptor.getValue();
assertThat(savedProduct.getName()).isEqualTo("Book");
assertThat(savedProduct.getPrice()).isEqualByComparingTo("100");
}
4. AssertJ — 流暢的斷言
// Collection assertions
assertThat(products)
.hasSize(3)
.extracting(ProductResponse::name)
.containsExactly("A", "B", "C");
// Exception assertions
assertThatThrownBy(() -> productService.getProduct(999L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessageContaining("Product")
.hasMessageContaining("999");
// Soft assertions — báo tất cả lỗi cùng lúc
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(result.id()).isEqualTo(1L);
softly.assertThat(result.name()).isEqualTo("Book");
softly.assertThat(result.price()).isPositive();
});
5. 測試服務層模式
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock private OrderRepository orderRepository;
@Mock private ProductRepository productRepository;
@Mock private ApplicationEventPublisher eventPublisher;
@InjectMocks private OrderService orderService;
@Test
void shouldCreateOrderAndPublishEvent() {
// Given
var request = new CreateOrderRequest(1L, List.of(
new OrderItemRequest(1L, 2),
new OrderItemRequest(2L, 1)
));
when(productRepository.findById(1L))
.thenReturn(Optional.of(product(1L, "A", 100)));
when(productRepository.findById(2L))
.thenReturn(Optional.of(product(2L, "B", 200)));
when(orderRepository.save(any(Order.class)))
.thenAnswer(inv -> {
Order o = inv.getArgument(0);
o.setId(1L);
return o;
});
// When
OrderResponse result = orderService.createOrder(request);
// Then
assertThat(result).isNotNull();
verify(orderRepository).save(any(Order.class));
verify(eventPublisher).publishEvent(any(OrderCreatedEvent.class));
}
private Product product(Long id, String name, int price) {
return Product.builder()
.id(id).name(name).price(new BigDecimal(price))
.build();
}
}
總結
- 單元測試遵循Given-When-Then模式,使用@Mock和@InjectMocks來隔離依賴關係
- JUnit Jupiter 6:@ParameterizedTest、@DisplayName、斷言 API
- Mockito:when().thenReturn()用於存根,verify()檢查交互,ArgumentCaptor捕獲參數
- AssertJ:集合、異常、軟斷言的流暢斷言
練習
1.為ProductService編寫單元測試:測試CRUD操作、模擬儲存庫、驗證足夠的場景(找到、找不到、驗證錯誤) 2. 為價格計算器編寫參數化測試:多種輸入/輸出組合 3. 測試 OrderService.createOrder():驗證訂單已儲存、事件已發布、庫存已減少。未找到產品時測試回滾