8.1 测试概述
8.1.1 测试金字塔
graph TD
A["UI 测试<br/>少量,高成本"] --> B["集成测试<br/>适量,中等成本"]
B --> C["单元测试<br/>大量,低成本"]
style A fill:#ff9999
style B fill:#ffcc99
style C fill:#99ff99
8.1.2 Quarkus 测试扩展
<!-- 核心测试依赖 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<!-- REST Assured 测试 -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试容器 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito 支持 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<!-- 性能测试 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-continuous-testing</artifactId>
<scope>test</scope>
</dependency>
8.2 单元测试
8.2.1 基础单元测试
package com.example.service;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@QuarkusTest
@DisplayName("用户服务测试")
class UserServiceTest {
@Inject
UserService userService;
@Test
@DisplayName("创建用户 - 成功场景")
void testCreateUser_Success() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("testuser");
request.setEmail("test@example.com");
request.setPassword("password123");
// When
UserResponse response = userService.createUser(request);
// Then
assertNotNull(response);
assertNotNull(response.getId());
assertEquals("testuser", response.getUsername());
assertEquals("test@example.com", response.getEmail());
assertNull(response.getPassword()); // 密码不应该返回
}
@Test
@DisplayName("创建用户 - 用户名已存在")
void testCreateUser_UsernameExists() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("existinguser");
request.setEmail("new@example.com");
request.setPassword("password123");
// 先创建一个用户
userService.createUser(request);
// When & Then
assertThrows(UserAlreadyExistsException.class, () -> {
userService.createUser(request);
});
}
@Test
@DisplayName("查找用户 - 按ID")
void testFindUserById() {
// Given
CreateUserRequest createRequest = new CreateUserRequest();
createRequest.setUsername("finduser");
createRequest.setEmail("find@example.com");
createRequest.setPassword("password123");
UserResponse createdUser = userService.createUser(createRequest);
// When
Optional<UserResponse> foundUser = userService.findById(createdUser.getId());
// Then
assertTrue(foundUser.isPresent());
assertEquals(createdUser.getId(), foundUser.get().getId());
assertEquals("finduser", foundUser.get().getUsername());
}
@Test
@DisplayName("查找用户 - 用户不存在")
void testFindUserById_NotFound() {
// When
Optional<UserResponse> foundUser = userService.findById(999L);
// Then
assertFalse(foundUser.isPresent());
}
@Nested
@DisplayName("用户验证测试")
class UserValidationTest {
@Test
@DisplayName("无效邮箱格式")
void testCreateUser_InvalidEmail() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("testuser");
request.setEmail("invalid-email");
request.setPassword("password123");
// When & Then
assertThrows(ValidationException.class, () -> {
userService.createUser(request);
});
}
@Test
@DisplayName("密码太短")
void testCreateUser_PasswordTooShort() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("testuser");
request.setEmail("test@example.com");
request.setPassword("123");
// When & Then
assertThrows(ValidationException.class, () -> {
userService.createUser(request);
});
}
@Test
@DisplayName("用户名为空")
void testCreateUser_EmptyUsername() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("");
request.setEmail("test@example.com");
request.setPassword("password123");
// When & Then
assertThrows(ValidationException.class, () -> {
userService.createUser(request);
});
}
}
@Nested
@DisplayName("用户更新测试")
class UserUpdateTest {
private UserResponse existingUser;
@BeforeEach
void setUp() {
CreateUserRequest createRequest = new CreateUserRequest();
createRequest.setUsername("updateuser");
createRequest.setEmail("update@example.com");
createRequest.setPassword("password123");
existingUser = userService.createUser(createRequest);
}
@Test
@DisplayName("更新用户信息 - 成功")
void testUpdateUser_Success() {
// Given
UpdateUserRequest updateRequest = new UpdateUserRequest();
updateRequest.setEmail("newemail@example.com");
updateRequest.setFirstName("John");
updateRequest.setLastName("Doe");
// When
UserResponse updatedUser = userService.updateUser(existingUser.getId(), updateRequest);
// Then
assertNotNull(updatedUser);
assertEquals(existingUser.getId(), updatedUser.getId());
assertEquals("newemail@example.com", updatedUser.getEmail());
assertEquals("John", updatedUser.getFirstName());
assertEquals("Doe", updatedUser.getLastName());
assertEquals("updateuser", updatedUser.getUsername()); // 用户名不变
}
@Test
@DisplayName("更新不存在的用户")
void testUpdateUser_NotFound() {
// Given
UpdateUserRequest updateRequest = new UpdateUserRequest();
updateRequest.setEmail("newemail@example.com");
// When & Then
assertThrows(UserNotFoundException.class, () -> {
userService.updateUser(999L, updateRequest);
});
}
}
}
8.2.2 Mock 测试
package com.example.service;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.mockito.ArgumentCaptor;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@QuarkusTest
@DisplayName("订单服务测试 - 使用 Mock")
class OrderServiceTest {
@Inject
OrderService orderService;
@InjectMock
UserRepository userRepository;
@InjectMock
ProductRepository productRepository;
@InjectMock
PaymentService paymentService;
@InjectMock
NotificationService notificationService;
private User mockUser;
private Product mockProduct;
@BeforeEach
void setUp() {
// 设置 Mock 用户
mockUser = new User();
mockUser.setId(1L);
mockUser.setUsername("testuser");
mockUser.setEmail("test@example.com");
// 设置 Mock 产品
mockProduct = new Product();
mockProduct.setId(1L);
mockProduct.setName("Test Product");
mockProduct.setPrice(new BigDecimal("99.99"));
mockProduct.setStock(10);
}
@Test
@DisplayName("创建订单 - 成功场景")
void testCreateOrder_Success() {
// Given
CreateOrderRequest request = new CreateOrderRequest();
request.setUserId(1L);
request.setProductId(1L);
request.setQuantity(2);
// Mock 依赖行为
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
when(productRepository.findById(1L)).thenReturn(Optional.of(mockProduct));
when(paymentService.processPayment(any(PaymentRequest.class)))
.thenReturn(new PaymentResult(true, "payment123"));
// When
OrderResponse response = orderService.createOrder(request);
// Then
assertNotNull(response);
assertNotNull(response.getId());
assertEquals(1L, response.getUserId());
assertEquals(1L, response.getProductId());
assertEquals(2, response.getQuantity());
assertEquals(new BigDecimal("199.98"), response.getTotalAmount());
assertEquals(OrderStatus.CONFIRMED, response.getStatus());
// 验证依赖调用
verify(userRepository).findById(1L);
verify(productRepository).findById(1L);
verify(paymentService).processPayment(any(PaymentRequest.class));
verify(notificationService).sendOrderConfirmation(eq(mockUser.getEmail()), any(OrderResponse.class));
}
@Test
@DisplayName("创建订单 - 用户不存在")
void testCreateOrder_UserNotFound() {
// Given
CreateOrderRequest request = new CreateOrderRequest();
request.setUserId(999L);
request.setProductId(1L);
request.setQuantity(2);
// Mock 依赖行为
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// When & Then
assertThrows(UserNotFoundException.class, () -> {
orderService.createOrder(request);
});
// 验证只调用了用户查找,没有调用其他服务
verify(userRepository).findById(999L);
verify(productRepository, never()).findById(anyLong());
verify(paymentService, never()).processPayment(any());
verify(notificationService, never()).sendOrderConfirmation(anyString(), any());
}
@Test
@DisplayName("创建订单 - 库存不足")
void testCreateOrder_InsufficientStock() {
// Given
CreateOrderRequest request = new CreateOrderRequest();
request.setUserId(1L);
request.setProductId(1L);
request.setQuantity(20); // 超过库存
// Mock 依赖行为
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
when(productRepository.findById(1L)).thenReturn(Optional.of(mockProduct));
// When & Then
assertThrows(InsufficientStockException.class, () -> {
orderService.createOrder(request);
});
// 验证没有调用支付和通知服务
verify(paymentService, never()).processPayment(any());
verify(notificationService, never()).sendOrderConfirmation(anyString(), any());
}
@Test
@DisplayName("创建订单 - 支付失败")
void testCreateOrder_PaymentFailed() {
// Given
CreateOrderRequest request = new CreateOrderRequest();
request.setUserId(1L);
request.setProductId(1L);
request.setQuantity(2);
// Mock 依赖行为
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
when(productRepository.findById(1L)).thenReturn(Optional.of(mockProduct));
when(paymentService.processPayment(any(PaymentRequest.class)))
.thenReturn(new PaymentResult(false, "Payment declined"));
// When & Then
assertThrows(PaymentFailedException.class, () -> {
orderService.createOrder(request);
});
// 验证支付被调用但通知没有被调用
verify(paymentService).processPayment(any(PaymentRequest.class));
verify(notificationService, never()).sendOrderConfirmation(anyString(), any());
}
@Test
@DisplayName("验证支付请求参数")
void testCreateOrder_VerifyPaymentRequest() {
// Given
CreateOrderRequest request = new CreateOrderRequest();
request.setUserId(1L);
request.setProductId(1L);
request.setQuantity(3);
// Mock 依赖行为
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
when(productRepository.findById(1L)).thenReturn(Optional.of(mockProduct));
when(paymentService.processPayment(any(PaymentRequest.class)))
.thenReturn(new PaymentResult(true, "payment123"));
// When
orderService.createOrder(request);
// Then - 使用 ArgumentCaptor 验证参数
ArgumentCaptor<PaymentRequest> paymentCaptor = ArgumentCaptor.forClass(PaymentRequest.class);
verify(paymentService).processPayment(paymentCaptor.capture());
PaymentRequest capturedPayment = paymentCaptor.getValue();
assertEquals(1L, capturedPayment.getUserId());
assertEquals(new BigDecimal("299.97"), capturedPayment.getAmount());
assertEquals("Test Product", capturedPayment.getDescription());
}
@Test
@DisplayName("取消订单 - 成功")
void testCancelOrder_Success() {
// Given
Long orderId = 1L;
Order existingOrder = new Order();
existingOrder.setId(orderId);
existingOrder.setStatus(OrderStatus.CONFIRMED);
existingOrder.setUserId(1L);
when(orderRepository.findById(orderId)).thenReturn(Optional.of(existingOrder));
when(paymentService.refundPayment(anyString())).thenReturn(true);
// When
OrderResponse response = orderService.cancelOrder(orderId);
// Then
assertNotNull(response);
assertEquals(OrderStatus.CANCELLED, response.getStatus());
// 验证退款和通知被调用
verify(paymentService).refundPayment(existingOrder.getPaymentId());
verify(notificationService).sendOrderCancellation(anyString(), any(OrderResponse.class));
}
}
8.2.3 参数化测试
package com.example.util;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("工具类测试")
class UtilityTest {
@ParameterizedTest
@DisplayName("邮箱验证测试")
@ValueSource(strings = {
"test@example.com",
"user.name@domain.co.uk",
"user+tag@example.org",
"123@456.com"
})
void testValidEmails(String email) {
assertTrue(EmailValidator.isValid(email), "应该是有效邮箱: " + email);
}
@ParameterizedTest
@DisplayName("无效邮箱测试")
@ValueSource(strings = {
"invalid-email",
"@example.com",
"test@",
"test..test@example.com",
"test@.com",
""
})
void testInvalidEmails(String email) {
assertFalse(EmailValidator.isValid(email), "应该是无效邮箱: " + email);
}
@ParameterizedTest
@DisplayName("密码强度测试")
@CsvSource({
"password123, WEAK",
"Password123, MEDIUM",
"Password123!, STRONG",
"P@ssw0rd123!, VERY_STRONG",
"123456, VERY_WEAK",
"abcdefgh, WEAK"
})
void testPasswordStrength(String password, PasswordStrength expectedStrength) {
PasswordStrength actualStrength = PasswordValidator.getStrength(password);
assertEquals(expectedStrength, actualStrength,
"密码 '" + password + "' 的强度应该是 " + expectedStrength);
}
@ParameterizedTest
@DisplayName("价格格式化测试")
@CsvSource({
"99.99, $99.99",
"1000.00, $1,000.00",
"0.50, $0.50",
"1234567.89, $1,234,567.89"
})
void testPriceFormatting(String input, String expected) {
BigDecimal price = new BigDecimal(input);
String formatted = PriceFormatter.format(price);
assertEquals(expected, formatted);
}
@ParameterizedTest
@DisplayName("日期解析测试")
@MethodSource("provideDateTestCases")
void testDateParsing(String input, LocalDate expected, boolean shouldSucceed) {
if (shouldSucceed) {
LocalDate actual = DateParser.parse(input);
assertEquals(expected, actual);
} else {
assertThrows(DateTimeParseException.class, () -> {
DateParser.parse(input);
});
}
}
static Stream<Arguments> provideDateTestCases() {
return Stream.of(
Arguments.of("2023-12-25", LocalDate.of(2023, 12, 25), true),
Arguments.of("25/12/2023", LocalDate.of(2023, 12, 25), true),
Arguments.of("Dec 25, 2023", LocalDate.of(2023, 12, 25), true),
Arguments.of("invalid-date", null, false),
Arguments.of("2023-13-01", null, false),
Arguments.of("", null, false)
);
}
@ParameterizedTest
@DisplayName("用户名验证测试")
@CsvFileSource(resources = "/test-data/username-validation.csv", numLinesToSkip = 1)
void testUsernameValidation(String username, boolean expected, String description) {
boolean actual = UsernameValidator.isValid(username);
assertEquals(expected, actual, description);
}
@ParameterizedTest
@DisplayName("数字范围测试")
@EnumSource(value = NumberRange.class, names = {"SMALL", "MEDIUM", "LARGE"})
void testNumberRanges(NumberRange range) {
assertTrue(range.getMin() < range.getMax());
assertTrue(range.contains(range.getMin()));
assertTrue(range.contains(range.getMax()));
assertFalse(range.contains(range.getMin() - 1));
assertFalse(range.contains(range.getMax() + 1));
}
}
8.3 集成测试
8.3.1 REST API 集成测试
package com.example.resource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.MethodOrderer;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@QuarkusTest
@TestHTTPEndpoint(UserResource.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("用户 REST API 集成测试")
class UserResourceIntegrationTest {
private String authToken;
private Long createdUserId;
@BeforeEach
void setUp() {
// 获取认证令牌
authToken = given()
.contentType(ContentType.JSON)
.body("""
{
"username": "admin",
"password": "admin123"
}
""")
.when()
.post("/auth/login")
.then()
.statusCode(200)
.extract()
.path("token");
}
@Test
@Order(1)
@DisplayName("创建用户 - 成功")
void testCreateUser_Success() {
createdUserId = given()
.header("Authorization", "Bearer " + authToken)
.contentType(ContentType.JSON)
.body("""
{
"username": "newuser",
"email": "newuser@example.com",
"password": "password123",
"firstName": "John",
"lastName": "Doe"
}
""")
.when()
.post()
.then()
.statusCode(201)
.contentType(ContentType.JSON)
.body("username", equalTo("newuser"))
.body("email", equalTo("newuser@example.com"))
.body("firstName", equalTo("John"))
.body("lastName", equalTo("Doe"))
.body("password", nullValue()) // 密码不应该返回
.body("id", notNullValue())
.body("createdAt", notNullValue())
.extract()
.path("id");
}
@Test
@Order(2)
@DisplayName("获取用户 - 按ID")
void testGetUser_ById() {
given()
.header("Authorization", "Bearer " + authToken)
.pathParam("id", createdUserId)
.when()
.get("/{id}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("id", equalTo(createdUserId.intValue()))
.body("username", equalTo("newuser"))
.body("email", equalTo("newuser@example.com"));
}
@Test
@Order(3)
@DisplayName("更新用户 - 成功")
void testUpdateUser_Success() {
given()
.header("Authorization", "Bearer " + authToken)
.pathParam("id", createdUserId)
.contentType(ContentType.JSON)
.body("""
{
"email": "updated@example.com",
"firstName": "Jane",
"lastName": "Smith"
}
""")
.when()
.put("/{id}")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("id", equalTo(createdUserId.intValue()))
.body("email", equalTo("updated@example.com"))
.body("firstName", equalTo("Jane"))
.body("lastName", equalTo("Smith"))
.body("username", equalTo("newuser")); // 用户名不变
}
@Test
@Order(4)
@DisplayName("获取用户列表 - 分页")
void testGetUsers_Paginated() {
given()
.header("Authorization", "Bearer " + authToken)
.queryParam("page", 0)
.queryParam("size", 10)
.queryParam("sort", "username")
.when()
.get()
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("content", hasSize(greaterThan(0)))
.body("totalElements", greaterThan(0))
.body("totalPages", greaterThan(0))
.body("size", equalTo(10))
.body("number", equalTo(0));
}
@Test
@Order(5)
@DisplayName("搜索用户 - 按用户名")
void testSearchUsers_ByUsername() {
given()
.header("Authorization", "Bearer " + authToken)
.queryParam("username", "newuser")
.when()
.get("/search")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("$", hasSize(1))
.body("[0].username", equalTo("newuser"));
}
@Test
@DisplayName("创建用户 - 验证失败")
void testCreateUser_ValidationFailure() {
given()
.header("Authorization", "Bearer " + authToken)
.contentType(ContentType.JSON)
.body("""
{
"username": "",
"email": "invalid-email",
"password": "123"
}
""")
.when()
.post()
.then()
.statusCode(400)
.contentType(ContentType.JSON)
.body("violations", hasSize(greaterThan(0)))
.body("violations.field", hasItems("username", "email", "password"));
}
@Test
@DisplayName("创建用户 - 用户名已存在")
void testCreateUser_UsernameExists() {
// 先创建一个用户
given()
.header("Authorization", "Bearer " + authToken)
.contentType(ContentType.JSON)
.body("""
{
"username": "duplicateuser",
"email": "duplicate1@example.com",
"password": "password123"
}
""")
.when()
.post()
.then()
.statusCode(201);
// 尝试创建相同用户名的用户
given()
.header("Authorization", "Bearer " + authToken)
.contentType(ContentType.JSON)
.body("""
{
"username": "duplicateuser",
"email": "duplicate2@example.com",
"password": "password123"
}
""")
.when()
.post()
.then()
.statusCode(409)
.contentType(ContentType.JSON)
.body("message", containsString("already exists"));
}
@Test
@DisplayName("获取用户 - 未找到")
void testGetUser_NotFound() {
given()
.header("Authorization", "Bearer " + authToken)
.pathParam("id", 99999)
.when()
.get("/{id}")
.then()
.statusCode(404)
.contentType(ContentType.JSON)
.body("message", containsString("not found"));
}
@Test
@DisplayName("未授权访问")
void testUnauthorizedAccess() {
given()
.contentType(ContentType.JSON)
.when()
.get()
.then()
.statusCode(401);
}
@Test
@DisplayName("无效令牌")
void testInvalidToken() {
given()
.header("Authorization", "Bearer invalid-token")
.when()
.get()
.then()
.statusCode(401);
}
@Test
@Order(6)
@DisplayName("删除用户 - 成功")
void testDeleteUser_Success() {
given()
.header("Authorization", "Bearer " + authToken)
.pathParam("id", createdUserId)
.when()
.delete("/{id}")
.then()
.statusCode(204);
// 验证用户已被删除
given()
.header("Authorization", "Bearer " + authToken)
.pathParam("id", createdUserId)
.when()
.get("/{id}")
.then()
.statusCode(404);
}
}
8.3.2 数据库集成测试
package com.example.repository;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.TestTransaction;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
@DisplayName("用户仓库集成测试")
class UserRepositoryIntegrationTest {
@Inject
UserRepository userRepository;
@Inject
EntityManager entityManager;
private User testUser;
@BeforeEach
@TestTransaction
void setUp() {
// 清理测试数据
entityManager.createQuery("DELETE FROM User u WHERE u.username LIKE 'test%'").executeUpdate();
// 创建测试用户
testUser = new User();
testUser.setUsername("testuser");
testUser.setEmail("test@example.com");
testUser.setPassword("hashedpassword");
testUser.setFirstName("Test");
testUser.setLastName("User");
}
@Test
@TestTransaction
@DisplayName("保存和查找用户")
void testSaveAndFindUser() {
// When
User savedUser = userRepository.save(testUser);
// Then
assertNotNull(savedUser.getId());
// 查找保存的用户
Optional<User> foundUser = userRepository.findById(savedUser.getId());
assertTrue(foundUser.isPresent());
assertEquals("testuser", foundUser.get().getUsername());
assertEquals("test@example.com", foundUser.get().getEmail());
}
@Test
@TestTransaction
@DisplayName("按用户名查找")
void testFindByUsername() {
// Given
userRepository.save(testUser);
// When
Optional<User> foundUser = userRepository.findByUsername("testuser");
// Then
assertTrue(foundUser.isPresent());
assertEquals("test@example.com", foundUser.get().getEmail());
}
@Test
@TestTransaction
@DisplayName("按邮箱查找")
void testFindByEmail() {
// Given
userRepository.save(testUser);
// When
Optional<User> foundUser = userRepository.findByEmail("test@example.com");
// Then
assertTrue(foundUser.isPresent());
assertEquals("testuser", foundUser.get().getUsername());
}
@Test
@TestTransaction
@DisplayName("检查用户名是否存在")
void testExistsByUsername() {
// Given
userRepository.save(testUser);
// When & Then
assertTrue(userRepository.existsByUsername("testuser"));
assertFalse(userRepository.existsByUsername("nonexistent"));
}
@Test
@TestTransaction
@DisplayName("检查邮箱是否存在")
void testExistsByEmail() {
// Given
userRepository.save(testUser);
// When & Then
assertTrue(userRepository.existsByEmail("test@example.com"));
assertFalse(userRepository.existsByEmail("nonexistent@example.com"));
}
@Test
@TestTransaction
@DisplayName("分页查询用户")
void testFindAllPaginated() {
// Given - 创建多个测试用户
for (int i = 1; i <= 15; i++) {
User user = new User();
user.setUsername("testuser" + i);
user.setEmail("test" + i + "@example.com");
user.setPassword("password");
userRepository.save(user);
}
// When
Page<User> firstPage = userRepository.findAll(PageRequest.of(0, 10));
Page<User> secondPage = userRepository.findAll(PageRequest.of(1, 10));
// Then
assertEquals(10, firstPage.getContent().size());
assertEquals(5, secondPage.getContent().size());
assertEquals(15, firstPage.getTotalElements());
assertEquals(2, firstPage.getTotalPages());
}
@Test
@TestTransaction
@DisplayName("按用户名搜索(模糊匹配)")
void testSearchByUsername() {
// Given
User user1 = new User();
user1.setUsername("testuser1");
user1.setEmail("test1@example.com");
user1.setPassword("password");
userRepository.save(user1);
User user2 = new User();
user2.setUsername("testuser2");
user2.setEmail("test2@example.com");
user2.setPassword("password");
userRepository.save(user2);
User user3 = new User();
user3.setUsername("otheruser");
user3.setEmail("other@example.com");
user3.setPassword("password");
userRepository.save(user3);
// When
List<User> results = userRepository.findByUsernameContaining("testuser");
// Then
assertEquals(2, results.size());
assertTrue(results.stream().allMatch(u -> u.getUsername().contains("testuser")));
}
@Test
@TestTransaction
@DisplayName("更新用户信息")
void testUpdateUser() {
// Given
User savedUser = userRepository.save(testUser);
// When
savedUser.setEmail("updated@example.com");
savedUser.setFirstName("Updated");
User updatedUser = userRepository.save(savedUser);
// Then
assertEquals("updated@example.com", updatedUser.getEmail());
assertEquals("Updated", updatedUser.getFirstName());
// 验证数据库中的数据
Optional<User> fromDb = userRepository.findById(savedUser.getId());
assertTrue(fromDb.isPresent());
assertEquals("updated@example.com", fromDb.get().getEmail());
assertEquals("Updated", fromDb.get().getFirstName());
}
@Test
@TestTransaction
@DisplayName("删除用户")
void testDeleteUser() {
// Given
User savedUser = userRepository.save(testUser);
Long userId = savedUser.getId();
// When
userRepository.deleteById(userId);
// Then
Optional<User> deletedUser = userRepository.findById(userId);
assertFalse(deletedUser.isPresent());
}
@Test
@TestTransaction
@DisplayName("自定义查询 - 按创建日期范围")
void testFindByCreatedDateRange() {
// Given
LocalDateTime now = LocalDateTime.now();
LocalDateTime yesterday = now.minusDays(1);
LocalDateTime tomorrow = now.plusDays(1);
testUser.setCreatedAt(now);
userRepository.save(testUser);
// When
List<User> results = userRepository.findByCreatedAtBetween(yesterday, tomorrow);
// Then
assertEquals(1, results.size());
assertEquals("testuser", results.get(0).getUsername());
}
@Test
@TestTransaction
@DisplayName("批量操作 - 更新用户状态")
void testBatchUpdateUserStatus() {
// Given
List<User> users = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
User user = new User();
user.setUsername("batchuser" + i);
user.setEmail("batch" + i + "@example.com");
user.setPassword("password");
user.setActive(true);
users.add(userRepository.save(user));
}
// When
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
int updatedCount = userRepository.updateActiveStatusByIds(userIds, false);
// Then
assertEquals(5, updatedCount);
// 验证更新结果
List<User> updatedUsers = userRepository.findAllById(userIds);
assertTrue(updatedUsers.stream().allMatch(u -> !u.isActive()));
}
}
8.4 测试容器
8.4.1 PostgreSQL 测试容器
package com.example.testcontainer;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.Map;
@QuarkusTest
@TestProfile(PostgreSQLTestProfile.class)
@Testcontainers
@DisplayName("PostgreSQL 测试容器集成测试")
class PostgreSQLIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass")
.withInitScript("test-data.sql");
@Inject
UserRepository userRepository;
@Test
@DisplayName("测试 PostgreSQL 连接")
void testPostgreSQLConnection() {
assertTrue(postgres.isRunning());
assertNotNull(postgres.getJdbcUrl());
}
@Test
@TestTransaction
@DisplayName("测试数据库操作")
void testDatabaseOperations() {
// Given
User user = new User();
user.setUsername("containeruser");
user.setEmail("container@example.com");
user.setPassword("password");
// When
User savedUser = userRepository.save(user);
// Then
assertNotNull(savedUser.getId());
Optional<User> foundUser = userRepository.findById(savedUser.getId());
assertTrue(foundUser.isPresent());
assertEquals("containeruser", foundUser.get().getUsername());
}
// 测试配置文件
public static class PostgreSQLTestProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
"quarkus.datasource.db-kind", "postgresql",
"quarkus.datasource.username", postgres.getUsername(),
"quarkus.datasource.password", postgres.getPassword(),
"quarkus.datasource.jdbc.url", postgres.getJdbcUrl(),
"quarkus.hibernate-orm.database.generation", "drop-and-create",
"quarkus.hibernate-orm.log.sql", "true"
);
}
}
}
8.4.2 Redis 测试容器
package com.example.testcontainer;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.Map;
@QuarkusTest
@TestProfile(RedisTestProfile.class)
@Testcontainers
@DisplayName("Redis 测试容器集成测试")
class RedisIntegrationTest {
@Container
static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
.withExposedPorts(6379)
.withCommand("redis-server", "--requirepass", "testpass");
@Inject
CacheService cacheService;
@Test
@DisplayName("测试 Redis 连接")
void testRedisConnection() {
assertTrue(redis.isRunning());
assertNotNull(redis.getHost());
assertTrue(redis.getMappedPort(6379) > 0);
}
@Test
@DisplayName("测试缓存操作")
void testCacheOperations() {
// Given
String key = "test-key";
String value = "test-value";
// When
cacheService.put(key, value);
// Then
Optional<String> cachedValue = cacheService.get(key);
assertTrue(cachedValue.isPresent());
assertEquals(value, cachedValue.get());
// Test deletion
cacheService.delete(key);
Optional<String> deletedValue = cacheService.get(key);
assertFalse(deletedValue.isPresent());
}
@Test
@DisplayName("测试缓存过期")
void testCacheExpiration() throws InterruptedException {
// Given
String key = "expire-key";
String value = "expire-value";
// When
cacheService.put(key, value, Duration.ofSeconds(1));
// Then
Optional<String> immediateValue = cacheService.get(key);
assertTrue(immediateValue.isPresent());
// Wait for expiration
Thread.sleep(1100);
Optional<String> expiredValue = cacheService.get(key);
assertFalse(expiredValue.isPresent());
}
// Redis 测试配置文件
public static class RedisTestProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
"quarkus.redis.hosts", "redis://" + redis.getHost() + ":" + redis.getMappedPort(6379),
"quarkus.redis.password", "testpass",
"quarkus.redis.timeout", "10s"
);
}
}
}
8.4.3 多容器测试环境
package com.example.testcontainer;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import java.util.Map;
@QuarkusTest
@TestProfile(MultiContainerTestProfile.class)
@Testcontainers
@DisplayName("多容器集成测试")
class MultiContainerIntegrationTest {
// 创建网络
static Network network = Network.newNetwork();
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withNetwork(network)
.withNetworkAliases("postgres")
.withDatabaseName("testdb")
.withUsername("testuser")
.withPassword("testpass");
@Container
static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7-alpine"))
.withNetwork(network)
.withNetworkAliases("redis")
.withExposedPorts(6379);
@Container
static GenericContainer<?> rabbitmq = new GenericContainer<>(DockerImageName.parse("rabbitmq:3-management-alpine"))
.withNetwork(network)
.withNetworkAliases("rabbitmq")
.withExposedPorts(5672, 15672)
.withEnv("RABBITMQ_DEFAULT_USER", "testuser")
.withEnv("RABBITMQ_DEFAULT_PASS", "testpass");
@Inject
UserService userService;
@Inject
CacheService cacheService;
@Inject
MessageService messageService;
@Test
@DisplayName("测试所有容器运行")
void testAllContainersRunning() {
assertTrue(postgres.isRunning());
assertTrue(redis.isRunning());
assertTrue(rabbitmq.isRunning());
}
@Test
@TestTransaction
@DisplayName("测试完整用户创建流程")
void testCompleteUserCreationFlow() {
// Given
CreateUserRequest request = new CreateUserRequest();
request.setUsername("integrationuser");
request.setEmail("integration@example.com");
request.setPassword("password123");
// When
UserResponse response = userService.createUser(request);
// Then
assertNotNull(response);
assertNotNull(response.getId());
// 验证数据库中的数据
Optional<User> dbUser = userService.findById(response.getId());
assertTrue(dbUser.isPresent());
// 验证缓存中的数据
Optional<String> cachedUser = cacheService.get("user:" + response.getId());
assertTrue(cachedUser.isPresent());
// 验证消息已发送
// 这里可以验证消息队列中是否有相应的消息
}
// 多容器测试配置文件
public static class MultiContainerTestProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
// PostgreSQL 配置
"quarkus.datasource.db-kind", "postgresql",
"quarkus.datasource.username", postgres.getUsername(),
"quarkus.datasource.password", postgres.getPassword(),
"quarkus.datasource.jdbc.url", postgres.getJdbcUrl(),
"quarkus.hibernate-orm.database.generation", "drop-and-create",
// Redis 配置
"quarkus.redis.hosts", "redis://" + redis.getHost() + ":" + redis.getMappedPort(6379),
// RabbitMQ 配置
"rabbitmq.host", rabbitmq.getHost(),
"rabbitmq.port", String.valueOf(rabbitmq.getMappedPort(5672)),
"rabbitmq.username", "testuser",
"rabbitmq.password", "testpass"
);
}
}
}
8.5 性能测试
8.5.1 JMH 基准测试
package com.example.benchmark;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
@Warmup(iterations = 3)
@Measurement(iterations = 5)
public class UserServiceBenchmark {
private UserService userService;
private CreateUserRequest createRequest;
private UpdateUserRequest updateRequest;
private Long existingUserId;
@Setup(Level.Trial)
public void setUp() {
// 初始化服务(这里需要手动设置依赖)
userService = new UserService();
// 准备测试数据
createRequest = new CreateUserRequest();
createRequest.setUsername("benchmarkuser");
createRequest.setEmail("benchmark@example.com");
createRequest.setPassword("password123");
updateRequest = new UpdateUserRequest();
updateRequest.setEmail("updated@example.com");
updateRequest.setFirstName("Updated");
// 创建一个用户用于更新测试
UserResponse user = userService.createUser(createRequest);
existingUserId = user.getId();
}
@Benchmark
public UserResponse benchmarkCreateUser() {
CreateUserRequest request = new CreateUserRequest();
request.setUsername("user" + System.nanoTime());
request.setEmail("user" + System.nanoTime() + "@example.com");
request.setPassword("password123");
return userService.createUser(request);
}
@Benchmark
public Optional<UserResponse> benchmarkFindUserById() {
return userService.findById(existingUserId);
}
@Benchmark
public UserResponse benchmarkUpdateUser() {
return userService.updateUser(existingUserId, updateRequest);
}
@Benchmark
public List<UserResponse> benchmarkFindAllUsers() {
return userService.findAll(PageRequest.of(0, 20)).getContent();
}
@Benchmark
@Group("mixed")
@GroupThreads(3)
public UserResponse benchmarkMixedCreate() {
return benchmarkCreateUser();
}
@Benchmark
@Group("mixed")
@GroupThreads(5)
public Optional<UserResponse> benchmarkMixedRead() {
return benchmarkFindUserById();
}
@Benchmark
@Group("mixed")
@GroupThreads(2)
public UserResponse benchmarkMixedUpdate() {
return benchmarkUpdateUser();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(UserServiceBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
8.5.2 负载测试
package com.example.loadtest;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.List;
import java.util.ArrayList;
import static io.restassured.RestAssured.given;
@QuarkusTest
@Execution(ExecutionMode.CONCURRENT)
@DisplayName("负载测试")
class LoadTest {
private static final int THREAD_COUNT = 50;
private static final int REQUESTS_PER_THREAD = 100;
private static final String AUTH_TOKEN = "test-token";
@Test
@DisplayName("用户创建负载测试")
void testUserCreationLoad() throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(THREAD_COUNT * REQUESTS_PER_THREAD);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
List<Long> responseTimes = new CopyOnWriteArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
executor.submit(() -> {
for (int j = 0; j < REQUESTS_PER_THREAD; j++) {
try {
long requestStart = System.currentTimeMillis();
given()
.header("Authorization", "Bearer " + AUTH_TOKEN)
.contentType(ContentType.JSON)
.body(String.format("""
{
"username": "loaduser_%d_%d",
"email": "load_%d_%d@example.com",
"password": "password123"
}
""", threadId, j, threadId, j))
.when()
.post("/api/users")
.then()
.statusCode(201);
long responseTime = System.currentTimeMillis() - requestStart;
responseTimes.add(responseTime);
successCount.incrementAndGet();
} catch (Exception e) {
errorCount.incrementAndGet();
} finally {
latch.countDown();
}
}
});
}
latch.await(5, TimeUnit.MINUTES);
executor.shutdown();
long totalTime = System.currentTimeMillis() - startTime;
int totalRequests = THREAD_COUNT * REQUESTS_PER_THREAD;
// 计算统计信息
double averageResponseTime = responseTimes.stream()
.mapToLong(Long::longValue)
.average()
.orElse(0.0);
long maxResponseTime = responseTimes.stream()
.mapToLong(Long::longValue)
.max()
.orElse(0L);
double throughput = (double) successCount.get() / (totalTime / 1000.0);
double errorRate = (double) errorCount.get() / totalRequests * 100;
// 输出测试结果
System.out.println("=== 负载测试结果 ===");
System.out.println("总请求数: " + totalRequests);
System.out.println("成功请求数: " + successCount.get());
System.out.println("失败请求数: " + errorCount.get());
System.out.println("错误率: " + String.format("%.2f%%", errorRate));
System.out.println("平均响应时间: " + String.format("%.2f ms", averageResponseTime));
System.out.println("最大响应时间: " + maxResponseTime + " ms");
System.out.println("吞吐量: " + String.format("%.2f requests/sec", throughput));
System.out.println("总耗时: " + totalTime + " ms");
// 断言
assertTrue(errorRate < 5.0, "错误率应该小于 5%");
assertTrue(averageResponseTime < 1000, "平均响应时间应该小于 1 秒");
assertTrue(throughput > 10, "吞吐量应该大于 10 requests/sec");
}
@Test
@DisplayName("并发读取测试")
void testConcurrentReads() throws InterruptedException {
// 先创建一些测试数据
List<Long> userIds = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Long userId = given()
.header("Authorization", "Bearer " + AUTH_TOKEN)
.contentType(ContentType.JSON)
.body(String.format("""
{
"username": "readuser_%d",
"email": "read_%d@example.com",
"password": "password123"
}
""", i, i))
.when()
.post("/api/users")
.then()
.statusCode(201)
.extract()
.path("id");
userIds.add(userId);
}
ExecutorService executor = Executors.newFixedThreadPool(20);
CountDownLatch latch = new CountDownLatch(200);
AtomicInteger successCount = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 200; i++) {
executor.submit(() -> {
try {
Long userId = userIds.get((int) (Math.random() * userIds.size()));
given()
.header("Authorization", "Bearer " + AUTH_TOKEN)
.pathParam("id", userId)
.when()
.get("/api/users/{id}")
.then()
.statusCode(200);
successCount.incrementAndGet();
} catch (Exception e) {
// 记录错误但不中断测试
} finally {
latch.countDown();
}
});
}
latch.await(2, TimeUnit.MINUTES);
executor.shutdown();
long totalTime = System.currentTimeMillis() - startTime;
double readThroughput = (double) successCount.get() / (totalTime / 1000.0);
System.out.println("读取吞吐量: " + String.format("%.2f reads/sec", readThroughput));
assertTrue(readThroughput > 50, "读取吞吐量应该大于 50 reads/sec");
}
}
8.6 持续测试
8.6.1 测试配置
# application-test.properties
# 数据库配置
quarkus.datasource.db-kind=h2
quarkus.datasource.username=sa
quarkus.datasource.password=
quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
# 测试端口
quarkus.http.test-port=8081
# 禁用不必要的功能
quarkus.mailer.mock=true
quarkus.scheduler.enabled=false
# 日志配置
quarkus.log.level=INFO
quarkus.log.category."com.example".level=DEBUG
# 安全配置
mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem
mp.jwt.verify.issuer=https://example.com/issuer
# 缓存配置(测试用内存缓存)
quarkus.cache.caffeine."user-cache".initial-capacity=100
quarkus.cache.caffeine."user-cache".maximum-size=1000
quarkus.cache.caffeine."user-cache".expire-after-write=PT30M
8.6.2 测试工具类
package com.example.test.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Set;
public class TestUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String JWT_SECRET = "test-secret-key-for-testing-purposes-only";
/**
* 生成测试用 JWT 令牌
*/
public static String generateTestToken(String username, Set<String> roles) {
Instant now = Instant.now();
return Jwts.builder()
.setSubject(username)
.claim("upn", username)
.claim("groups", roles)
.setIssuedAt(Date.from(now))
.setExpirationDate(Date.from(now.plus(1, ChronoUnit.HOURS)))
.signWith(SignatureAlgorithm.HS256, JWT_SECRET)
.compact();
}
/**
* 生成管理员令牌
*/
public static String generateAdminToken() {
return generateTestToken("admin", Set.of("admin", "user"));
}
/**
* 生成普通用户令牌
*/
public static String generateUserToken() {
return generateTestToken("user", Set.of("user"));
}
/**
* 将对象转换为 JSON 字符串
*/
public static String toJson(Object object) {
try {
return objectMapper.writeValueAsString(object);
} catch (Exception e) {
throw new RuntimeException("Failed to convert object to JSON", e);
}
}
/**
* 从 JSON 字符串解析对象
*/
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return objectMapper.readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException("Failed to parse JSON to object", e);
}
}
/**
* 生成随机用户数据
*/
public static CreateUserRequest generateRandomUser() {
String timestamp = String.valueOf(System.currentTimeMillis());
CreateUserRequest request = new CreateUserRequest();
request.setUsername("user" + timestamp);
request.setEmail("user" + timestamp + "@example.com");
request.setPassword("password123");
request.setFirstName("Test");
request.setLastName("User");
return request;
}
/**
* 等待异步操作完成
*/
public static void waitForAsyncOperation(int milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting", e);
}
}
/**
* 重试操作直到成功或超时
*/
public static <T> T retryUntilSuccess(Supplier<T> operation, int maxAttempts, int delayMs) {
Exception lastException = null;
for (int i = 0; i < maxAttempts; i++) {
try {
return operation.get();
} catch (Exception e) {
lastException = e;
if (i < maxAttempts - 1) {
waitForAsyncOperation(delayMs);
}
}
}
throw new RuntimeException("Operation failed after " + maxAttempts + " attempts", lastException);
}
/**
* 验证响应时间
*/
public static void assertResponseTime(Runnable operation, long maxTimeMs) {
long startTime = System.currentTimeMillis();
operation.run();
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
assertTrue(responseTime <= maxTimeMs,
"Response time " + responseTime + "ms exceeded maximum " + maxTimeMs + "ms");
}
}
8.6.3 测试数据构建器
package com.example.test.builder;
import java.time.LocalDateTime;
import java.math.BigDecimal;
public class UserTestDataBuilder {
private String username = "testuser";
private String email = "test@example.com";
private String password = "password123";
private String firstName = "Test";
private String lastName = "User";
private boolean active = true;
private LocalDateTime createdAt = LocalDateTime.now();
private Set<String> roles = Set.of("user");
public static UserTestDataBuilder aUser() {
return new UserTestDataBuilder();
}
public UserTestDataBuilder withUsername(String username) {
this.username = username;
return this;
}
public UserTestDataBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserTestDataBuilder withPassword(String password) {
this.password = password;
return this;
}
public UserTestDataBuilder withName(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
return this;
}
public UserTestDataBuilder inactive() {
this.active = false;
return this;
}
public UserTestDataBuilder withRoles(String... roles) {
this.roles = Set.of(roles);
return this;
}
public UserTestDataBuilder admin() {
return withRoles("admin", "user");
}
public UserTestDataBuilder createdAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
return this;
}
public CreateUserRequest buildRequest() {
CreateUserRequest request = new CreateUserRequest();
request.setUsername(username);
request.setEmail(email);
request.setPassword(password);
request.setFirstName(firstName);
request.setLastName(lastName);
return request;
}
public User buildEntity() {
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setPassword(password);
user.setFirstName(firstName);
user.setLastName(lastName);
user.setActive(active);
user.setCreatedAt(createdAt);
return user;
}
}
public class OrderTestDataBuilder {
private Long userId = 1L;
private Long productId = 1L;
private int quantity = 1;
private BigDecimal unitPrice = new BigDecimal("99.99");
private OrderStatus status = OrderStatus.PENDING;
private LocalDateTime createdAt = LocalDateTime.now();
public static OrderTestDataBuilder anOrder() {
return new OrderTestDataBuilder();
}
public OrderTestDataBuilder forUser(Long userId) {
this.userId = userId;
return this;
}
public OrderTestDataBuilder forProduct(Long productId) {
this.productId = productId;
return this;
}
public OrderTestDataBuilder withQuantity(int quantity) {
this.quantity = quantity;
return this;
}
public OrderTestDataBuilder withPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
return this;
}
public OrderTestDataBuilder confirmed() {
this.status = OrderStatus.CONFIRMED;
return this;
}
public OrderTestDataBuilder cancelled() {
this.status = OrderStatus.CANCELLED;
return this;
}
public OrderTestDataBuilder shipped() {
this.status = OrderStatus.SHIPPED;
return this;
}
public CreateOrderRequest buildRequest() {
CreateOrderRequest request = new CreateOrderRequest();
request.setUserId(userId);
request.setProductId(productId);
request.setQuantity(quantity);
return request;
}
public Order buildEntity() {
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setUnitPrice(unitPrice);
order.setTotalAmount(unitPrice.multiply(BigDecimal.valueOf(quantity)));
order.setStatus(status);
order.setCreatedAt(createdAt);
return order;
}
}
8.7 测试报告与分析
8.7.1 JaCoCo 代码覆盖率
<!-- pom.xml 中添加 JaCoCo 插件 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</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>
8.7.2 Surefire 测试报告
<!-- Maven Surefire 插件配置 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M9</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/*IntegrationTest.java</exclude>
</excludes>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<reportFormat>xml</reportFormat>
<reportFormat>plain</reportFormat>
</configuration>
</plugin>
<!-- Failsafe 插件用于集成测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0-M9</version>
<configuration>
<includes>
<include>**/*IntegrationTest.java</include>
<include>**/*IT.java</include>
</includes>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemPropertyVariables>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
8.8 本章小结
8.8.1 核心概念回顾
- 测试金字塔:单元测试为基础,集成测试为中层,端到端测试为顶层
- 测试分类:单元测试、集成测试、性能测试、负载测试
- 测试工具:JUnit 5、REST Assured、Mockito、TestContainers
- 测试策略:TDD、BDD、持续测试
8.8.2 技术要点总结
单元测试最佳实践
- 使用
@QuarkusTest
注解 - 合理使用 Mock 和 Stub
- 编写可读性强的测试用例
- 使用参数化测试提高覆盖率
- 使用
集成测试策略
- REST API 端到端测试
- 数据库集成测试
- 使用 TestContainers 模拟外部依赖
- 多容器环境测试
性能测试方法
- JMH 基准测试
- 负载测试和压力测试
- 响应时间和吞吐量监控
- 性能回归检测
测试工具链
- JaCoCo 代码覆盖率分析
- Surefire/Failsafe 测试报告
- 持续集成中的测试自动化
8.8.3 最佳实践
- 遵循测试金字塔原则,重点关注单元测试
- 使用测试数据构建器模式提高测试代码复用性
- 合理使用测试配置文件隔离测试环境
- 定期进行性能基准测试,及时发现性能回归
- 保持测试代码的简洁性和可维护性
8.8.4 下一章预告
下一章将学习 容器化与云原生部署,包括: - Docker 容器化实践 - Kubernetes 部署策略 - 云原生配置管理 - 服务网格集成 - 监控和日志聚合