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 技术要点总结

  1. 单元测试最佳实践

    • 使用 @QuarkusTest 注解
    • 合理使用 Mock 和 Stub
    • 编写可读性强的测试用例
    • 使用参数化测试提高覆盖率
  2. 集成测试策略

    • REST API 端到端测试
    • 数据库集成测试
    • 使用 TestContainers 模拟外部依赖
    • 多容器环境测试
  3. 性能测试方法

    • JMH 基准测试
    • 负载测试和压力测试
    • 响应时间和吞吐量监控
    • 性能回归检测
  4. 测试工具链

    • JaCoCo 代码覆盖率分析
    • Surefire/Failsafe 测试报告
    • 持续集成中的测试自动化

8.8.3 最佳实践

  • 遵循测试金字塔原则,重点关注单元测试
  • 使用测试数据构建器模式提高测试代码复用性
  • 合理使用测试配置文件隔离测试环境
  • 定期进行性能基准测试,及时发现性能回归
  • 保持测试代码的简洁性和可维护性

8.8.4 下一章预告

下一章将学习 容器化与云原生部署,包括: - Docker 容器化实践 - Kubernetes 部署策略 - 云原生配置管理 - 服务网格集成 - 监控和日志聚合