Atlas Mapper 教程系列 (7/10):单元测试与集成测试

🎯 学习目标

通过本篇教程,你将学会:

  • 掌握 Atlas Mapper 的单元测试编写方法
  • 学会使用 Mock 和测试数据进行测试
  • 理解集成测试的设计和实现
  • 掌握测试覆盖率分析和质量保证

📋 概念讲解:测试策略架构

测试金字塔

测试类型和范围

flowchart LR subgraph "单元测试" A1[Mapper接口测试] A2[类型转换器测试] A3[映射规则测试] A4[边界条件测试] end subgraph "集成测试" B1[Spring容器测试] B2[数据库集成测试] B3[Service层测试] B4[Controller层测试] end subgraph "端到端测试" C1[API测试] C2[业务流程测试] C3[性能测试] C4[兼容性测试] end A1 --> B1 A2 --> B2 A3 --> B3 A4 --> B4 B1 --> C1 B2 --> C2 B3 --> C3 B4 --> C4 style A1 fill:#c8e6c9 style B1 fill:#e8f5e8 style C1 fill:#fff3e0

🔧 实现步骤:单元测试详解

步骤 1:测试环境搭建

添加测试依赖

xml 复制代码
<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot Test Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- Atlas Mapper Test -->
    <dependency>
        <groupId>io.github.nemoob</groupId>
        <artifactId>atlas-mapper-test</artifactId>
        <version>1.0.0</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Testcontainers (可选,用于集成测试) -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>mysql</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- MockWebServer (可选,用于外部API测试) -->
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>mockwebserver</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

测试配置文件

yaml 复制代码
# src/test/resources/application-test.yml
spring:
  profiles:
    active: test
  
  # 测试数据源配置
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  
  # JPA 测试配置
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        dialect: org.hibernate.dialect.H2Dialect

# Atlas Mapper 测试配置
atlas:
  mapper:
    enabled: true
    verbose: true                    # 测试环境启用详细日志
    show-generated-code: true        # 显示生成代码便于调试
    performance-monitoring: false    # 测试环境关闭性能监控
    
# 日志配置
logging:
  level:
    io.github.nemoob.atlas.mapper: DEBUG
    org.springframework.test: DEBUG
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

步骤 2:Mapper 单元测试

基础 Mapper 测试

java 复制代码
/**
 * 用户映射器单元测试
 */
@ExtendWith(MockitoExtension.class)
class UserMapperTest {
    
    // 🔥 使用 Mappers.getMapper() 获取映射器实例
    private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
    
    /**
     * 测试基本映射功能
     */
    @Test
    @DisplayName("测试用户实体到DTO的基本映射")
    void testBasicEntityToDto() {
        // Given - 准备测试数据
        User user = createTestUser();
        
        // When - 执行映射
        UserDto dto = userMapper.toDto(user);
        
        // Then - 验证结果
        assertThat(dto).isNotNull();
        assertThat(dto.getId()).isEqualTo(user.getId());
        assertThat(dto.getName()).isEqualTo(user.getName());
        assertThat(dto.getEmail()).isEqualTo(user.getEmail());
        
        // 🔥 使用 AssertJ 的软断言
        assertThat(dto)
                .extracting("id", "name", "email")
                .containsExactly(user.getId(), user.getName(), user.getEmail());
    }
    
    /**
     * 测试反向映射
     */
    @Test
    @DisplayName("测试DTO到用户实体的反向映射")
    void testBasicDtoToEntity() {
        // Given
        UserDto dto = createTestUserDto();
        
        // When
        User entity = userMapper.toEntity(dto);
        
        // Then
        assertThat(entity).isNotNull();
        assertThat(entity.getId()).isEqualTo(dto.getId());
        assertThat(entity.getName()).isEqualTo(dto.getName());
        assertThat(entity.getEmail()).isEqualTo(dto.getEmail());
    }
    
    /**
     * 测试空值处理
     */
    @Test
    @DisplayName("测试空值和null值的处理")
    void testNullValueHandling() {
        // Given - null 对象
        User nullUser = null;
        
        // When
        UserDto dto = userMapper.toDto(nullUser);
        
        // Then
        assertThat(dto).isNull();
        
        // Given - 部分字段为 null 的对象
        User userWithNulls = new User();
        userWithNulls.setId(1L);
        userWithNulls.setName(null);  // null 字段
        userWithNulls.setEmail("test@example.com");
        
        // When
        UserDto dtoWithNulls = userMapper.toDto(userWithNulls);
        
        // Then
        assertThat(dtoWithNulls).isNotNull();
        assertThat(dtoWithNulls.getId()).isEqualTo(1L);
        assertThat(dtoWithNulls.getName()).isNull();
        assertThat(dtoWithNulls.getEmail()).isEqualTo("test@example.com");
    }
    
    /**
     * 测试集合映射
     */
    @Test
    @DisplayName("测试用户列表的映射")
    void testListMapping() {
        // Given
        List<User> users = Arrays.asList(
                createTestUser(1L, "张三", "zhangsan@example.com"),
                createTestUser(2L, "李四", "lisi@example.com"),
                createTestUser(3L, "王五", "wangwu@example.com")
        );
        
        // When
        List<UserDto> dtos = userMapper.toDtoList(users);
        
        // Then
        assertThat(dtos).hasSize(3);
        assertThat(dtos)
                .extracting("name")
                .containsExactly("张三", "李四", "王五");
        
        // 验证每个元素的映射
        for (int i = 0; i < users.size(); i++) {
            User user = users.get(i);
            UserDto dto = dtos.get(i);
            
            assertThat(dto.getId()).isEqualTo(user.getId());
            assertThat(dto.getName()).isEqualTo(user.getName());
            assertThat(dto.getEmail()).isEqualTo(user.getEmail());
        }
    }
    
    /**
     * 测试空集合映射
     */
    @Test
    @DisplayName("测试空集合和null集合的映射")
    void testEmptyAndNullListMapping() {
        // Given - null 列表
        List<User> nullList = null;
        
        // When
        List<UserDto> nullResult = userMapper.toDtoList(nullList);
        
        // Then
        assertThat(nullResult).isNull();
        
        // Given - 空列表
        List<User> emptyList = Collections.emptyList();
        
        // When
        List<UserDto> emptyResult = userMapper.toDtoList(emptyList);
        
        // Then
        assertThat(emptyResult).isEmpty();
    }
    
    // 辅助方法
    private User createTestUser() {
        return createTestUser(1L, "测试用户", "test@example.com");
    }
    
    private User createTestUser(Long id, String name, String email) {
        User user = new User();
        user.setId(id);
        user.setName(name);
        user.setEmail(email);
        user.setCreatedAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());
        return user;
    }
    
    private UserDto createTestUserDto() {
        UserDto dto = new UserDto();
        dto.setId(1L);
        dto.setName("测试用户");
        dto.setEmail("test@example.com");
        return dto;
    }
}

复杂映射测试

java 复制代码
/**
 * 复杂映射场景测试
 */
@ExtendWith(MockitoExtension.class)
class ComplexMappingTest {
    
    private final OrderMapper orderMapper = Mappers.getMapper(OrderMapper.class);
    private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
    
    /**
     * 测试嵌套对象映射
     */
    @Test
    @DisplayName("测试订单嵌套对象映射")
    void testNestedObjectMapping() {
        // Given
        Order order = createComplexOrder();
        
        // When
        OrderDto dto = orderMapper.toDto(order);
        
        // Then
        assertThat(dto).isNotNull();
        assertThat(dto.getId()).isEqualTo(order.getId());
        assertThat(dto.getOrderNo()).isEqualTo(order.getOrderNo());
        
        // 验证嵌套的客户信息
        assertThat(dto.getCustomer()).isNotNull();
        assertThat(dto.getCustomer().getId()).isEqualTo(order.getCustomer().getId());
        assertThat(dto.getCustomer().getName()).isEqualTo(order.getCustomer().getName());
        
        // 验证嵌套的地址信息
        assertThat(dto.getCustomer().getAddress()).isNotNull();
        assertThat(dto.getCustomer().getAddress().getProvince())
                .isEqualTo(order.getCustomer().getAddress().getProvince());
    }
    
    /**
     * 测试集合嵌套映射
     */
    @Test
    @DisplayName("测试订单项集合映射")
    void testNestedCollectionMapping() {
        // Given
        Order order = createOrderWithItems();
        
        // When
        OrderDto dto = orderMapper.toDto(order);
        
        // Then
        assertThat(dto.getItems()).hasSize(order.getItems().size());
        
        for (int i = 0; i < order.getItems().size(); i++) {
            OrderItem item = order.getItems().get(i);
            OrderItemDto itemDto = dto.getItems().get(i);
            
            assertThat(itemDto.getId()).isEqualTo(item.getId());
            assertThat(itemDto.getQuantity()).isEqualTo(item.getQuantity());
            
            // 验证嵌套的产品信息
            assertThat(itemDto.getProduct()).isNotNull();
            assertThat(itemDto.getProduct().getId()).isEqualTo(item.getProduct().getId());
        }
    }
    
    /**
     * 测试自定义映射方法
     */
    @Test
    @DisplayName("测试自定义映射方法和表达式")
    void testCustomMappingMethods() {
        // Given
        Order order = createOrderWithCalculatedFields();
        
        // When
        OrderDto dto = orderMapper.toDto(order);
        
        // Then
        // 验证计算字段
        assertThat(dto.getTotalAmount()).isNotNull();
        assertThat(dto.getTotalItems()).isGreaterThan(0);
        
        // 验证格式化字段
        assertThat(dto.getCreatedAt()).matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}");
        
        // 验证自定义转换
        if (order.getStatus() != null) {
            assertThat(dto.getStatusDesc()).isNotBlank();
        }
    }
    
    /**
     * 测试循环引用处理
     */
    @Test
    @DisplayName("测试循环引用的处理")
    void testCircularReferenceHandling() {
        // Given - 创建有循环引用的对象
        Category parent = new Category();
        parent.setId(1L);
        parent.setName("父分类");
        
        Category child = new Category();
        child.setId(2L);
        child.setName("子分类");
        child.setParent(parent);
        
        parent.setChildren(Arrays.asList(child));
        
        // When - 使用浅层映射避免循环引用
        CategoryMapper categoryMapper = Mappers.getMapper(CategoryMapper.class);
        CategoryDto dto = categoryMapper.toShallowDto(parent);
        
        // Then
        assertThat(dto).isNotNull();
        assertThat(dto.getId()).isEqualTo(parent.getId());
        assertThat(dto.getName()).isEqualTo(parent.getName());
        
        // 验证循环引用字段被忽略
        assertThat(dto.getParent()).isNull();
        assertThat(dto.getChildren()).isNull();
    }
    
    // 辅助方法
    private Order createComplexOrder() {
        // 创建地址
        Address address = new Address();
        address.setProvince("广东省");
        address.setCity("深圳市");
        address.setDistrict("南山区");
        address.setDetail("科技园");
        
        // 创建客户
        UserWithAddress customer = new UserWithAddress();
        customer.setId(1L);
        customer.setName("张三");
        customer.setEmail("zhangsan@example.com");
        customer.setAddress(address);
        
        // 创建订单
        Order order = new Order();
        order.setId(1001L);
        order.setOrderNo("ORD20250109001");
        order.setCustomer(customer);
        order.setCreatedAt(LocalDateTime.now());
        order.setStatus(1);
        
        return order;
    }
    
    private Order createOrderWithItems() {
        Order order = createComplexOrder();
        
        // 创建产品
        Product product1 = new Product();
        product1.setId(1L);
        product1.setName("iPhone 15");
        product1.setPrice(new BigDecimal("8999.00"));
        
        Product product2 = new Product();
        product2.setId(2L);
        product2.setName("保护壳");
        product2.setPrice(new BigDecimal("99.00"));
        
        // 创建订单项
        OrderItem item1 = new OrderItem();
        item1.setId(1L);
        item1.setProduct(product1);
        item1.setQuantity(1);
        item1.setUnitPrice(product1.getPrice());
        
        OrderItem item2 = new OrderItem();
        item2.setId(2L);
        item2.setProduct(product2);
        item2.setQuantity(2);
        item2.setUnitPrice(product2.getPrice());
        
        order.setItems(Arrays.asList(item1, item2));
        
        return order;
    }
    
    private Order createOrderWithCalculatedFields() {
        Order order = createOrderWithItems();
        order.setMetadata(Map.of("source", "mobile", "channel", "app"));
        order.setTags(Set.of("urgent", "vip"));
        return order;
    }
}

步骤 3:类型转换器测试

java 复制代码
/**
 * 自定义类型转换器测试
 */
@ExtendWith(MockitoExtension.class)
class CustomTypeConverterTest {
    
    private final CustomTypeConverter converter = new CustomTypeConverter();
    
    /**
     * 测试状态码转换
     */
    @Test
    @DisplayName("测试状态码到描述的转换")
    void testStatusCodeToDescription() {
        // 测试正常值
        assertThat(converter.statusCodeToDescription(0)).isEqualTo("待处理");
        assertThat(converter.statusCodeToDescription(1)).isEqualTo("处理中");
        assertThat(converter.statusCodeToDescription(2)).isEqualTo("已完成");
        assertThat(converter.statusCodeToDescription(3)).isEqualTo("已取消");
        
        // 测试边界值
        assertThat(converter.statusCodeToDescription(null)).isEqualTo("未知状态");
        assertThat(converter.statusCodeToDescription(-1)).isEqualTo("未知状态");
        assertThat(converter.statusCodeToDescription(999)).isEqualTo("未知状态");
    }
    
    /**
     * 测试反向转换
     */
    @Test
    @DisplayName("测试描述到状态码的反向转换")
    void testDescriptionToStatusCode() {
        // 测试正常值
        assertThat(converter.descriptionToStatusCode("待处理")).isEqualTo(0);
        assertThat(converter.descriptionToStatusCode("处理中")).isEqualTo(1);
        assertThat(converter.descriptionToStatusCode("已完成")).isEqualTo(2);
        assertThat(converter.descriptionToStatusCode("已取消")).isEqualTo(3);
        
        // 测试边界值
        assertThat(converter.descriptionToStatusCode(null)).isEqualTo(0);
        assertThat(converter.descriptionToStatusCode("")).isEqualTo(0);
        assertThat(converter.descriptionToStatusCode("未知状态")).isEqualTo(0);
    }
    
    /**
     * 测试金额转换
     */
    @Test
    @DisplayName("测试分到元的金额转换")
    void testCentToYuan() {
        // 测试正常值
        assertThat(converter.centToYuan(100L)).isEqualTo("¥1.00");
        assertThat(converter.centToYuan(12345L)).isEqualTo("¥123.45");
        assertThat(converter.centToYuan(999999L)).isEqualTo("¥9999.99");
        
        // 测试边界值
        assertThat(converter.centToYuan(0L)).isEqualTo("¥0.00");
        assertThat(converter.centToYuan(null)).isEqualTo("¥0.00");
        
        // 测试精度
        assertThat(converter.centToYuan(1L)).isEqualTo("¥0.01");
        assertThat(converter.centToYuan(99L)).isEqualTo("¥0.99");
    }
    
    /**
     * 测试地址转换
     */
    @Test
    @DisplayName("测试地址对象到字符串的转换")
    void testAddressToString() {
        // Given - 完整地址
        Address fullAddress = new Address();
        fullAddress.setProvince("广东省");
        fullAddress.setCity("深圳市");
        fullAddress.setDistrict("南山区");
        fullAddress.setDetail("科技园南区");
        
        // When
        String result = converter.addressToString(fullAddress);
        
        // Then
        assertThat(result).isEqualTo("广东省深圳市南山区科技园南区");
        
        // Given - 部分地址
        Address partialAddress = new Address();
        partialAddress.setProvince("北京市");
        partialAddress.setCity("北京市");
        
        // When
        String partialResult = converter.addressToString(partialAddress);
        
        // Then
        assertThat(partialResult).isEqualTo("北京市北京市");
        
        // Given - null 地址
        String nullResult = converter.addressToString(null);
        
        // Then
        assertThat(nullResult).isEmpty();
    }
    
    /**
     * 测试时间戳转换
     */
    @Test
    @DisplayName("测试时间戳到相对时间的转换")
    void testTimestampToRelativeTime() {
        long now = System.currentTimeMillis();
        
        // 测试不同时间间隔
        assertThat(converter.timestampToRelativeTime(now - 30 * 1000)).isEqualTo("刚刚");  // 30秒前
        assertThat(converter.timestampToRelativeTime(now - 5 * 60 * 1000)).isEqualTo("5分钟前");  // 5分钟前
        assertThat(converter.timestampToRelativeTime(now - 2 * 60 * 60 * 1000)).isEqualTo("2小时前");  // 2小时前
        assertThat(converter.timestampToRelativeTime(now - 3 * 24 * 60 * 60 * 1000)).isEqualTo("3天前");  // 3天前
        
        // 测试边界值
        assertThat(converter.timestampToRelativeTime(null)).isEqualTo("未知时间");
    }
}

💻 示例代码:集成测试详解

示例 1:Spring Boot 集成测试

java 复制代码
/**
 * Spring Boot 集成测试
 */
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.yml")
@Transactional
@Rollback
class UserServiceIntegrationTest {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private TestEntityManager testEntityManager;
    
    /**
     * 测试用户创建的完整流程
     */
    @Test
    @DisplayName("测试用户创建的完整流程")
    void testCreateUserCompleteFlow() {
        // Given
        UserDto inputDto = new UserDto();
        inputDto.setName("集成测试用户");
        inputDto.setEmail("integration@example.com");
        
        // When
        UserDto resultDto = userService.createUser(inputDto);
        
        // Then
        assertThat(resultDto).isNotNull();
        assertThat(resultDto.getId()).isNotNull();
        assertThat(resultDto.getName()).isEqualTo(inputDto.getName());
        assertThat(resultDto.getEmail()).isEqualTo(inputDto.getEmail());
        
        // 验证数据库中的数据
        Optional<User> savedUser = userRepository.findById(resultDto.getId());
        assertThat(savedUser).isPresent();
        assertThat(savedUser.get().getName()).isEqualTo(inputDto.getName());
        assertThat(savedUser.get().getEmail()).isEqualTo(inputDto.getEmail());
        assertThat(savedUser.get().getCreatedAt()).isNotNull();
        assertThat(savedUser.get().getUpdatedAt()).isNotNull();
    }
    
    /**
     * 测试用户更新流程
     */
    @Test
    @DisplayName("测试用户更新流程")
    void testUpdateUserFlow() {
        // Given - 先创建一个用户
        User existingUser = new User();
        existingUser.setName("原始用户");
        existingUser.setEmail("original@example.com");
        existingUser.setCreatedAt(LocalDateTime.now());
        existingUser.setUpdatedAt(LocalDateTime.now());
        
        User savedUser = testEntityManager.persistAndFlush(existingUser);
        
        // 准备更新数据
        UserDto updateDto = new UserDto();
        updateDto.setName("更新后用户");
        updateDto.setEmail("updated@example.com");
        
        // When
        UserDto resultDto = userService.updateUser(savedUser.getId(), updateDto);
        
        // Then
        assertThat(resultDto).isNotNull();
        assertThat(resultDto.getId()).isEqualTo(savedUser.getId());
        assertThat(resultDto.getName()).isEqualTo(updateDto.getName());
        assertThat(resultDto.getEmail()).isEqualTo(updateDto.getEmail());
        
        // 验证数据库中的数据
        testEntityManager.clear();  // 清除一级缓存
        User updatedUser = testEntityManager.find(User.class, savedUser.getId());
        assertThat(updatedUser.getName()).isEqualTo(updateDto.getName());
        assertThat(updatedUser.getEmail()).isEqualTo(updateDto.getEmail());
        assertThat(updatedUser.getUpdatedAt()).isAfter(updatedUser.getCreatedAt());
    }
    
    /**
     * 测试批量操作
     */
    @Test
    @DisplayName("测试批量用户转换")
    void testBatchUserConversion() {
        // Given - 创建多个用户
        List<User> users = Arrays.asList(
                createAndSaveUser("用户1", "user1@example.com"),
                createAndSaveUser("用户2", "user2@example.com"),
                createAndSaveUser("用户3", "user3@example.com")
        );
        
        // When
        List<UserDto> dtos = userService.convertUsers(users);
        
        // Then
        assertThat(dtos).hasSize(3);
        
        for (int i = 0; i < users.size(); i++) {
            User user = users.get(i);
            UserDto dto = dtos.get(i);
            
            assertThat(dto.getId()).isEqualTo(user.getId());
            assertThat(dto.getName()).isEqualTo(user.getName());
            assertThat(dto.getEmail()).isEqualTo(user.getEmail());
        }
    }
    
    /**
     * 测试异常情况
     */
    @Test
    @DisplayName("测试用户不存在的异常情况")
    void testUserNotFoundExceptionHandling() {
        // Given
        Long nonExistentId = 99999L;
        
        // When & Then
        assertThatThrownBy(() -> userService.getUserById(nonExistentId))
                .isInstanceOf(EntityNotFoundException.class)
                .hasMessageContaining("用户不存在: " + nonExistentId);
    }
    
    // 辅助方法
    private User createAndSaveUser(String name, String email) {
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        user.setCreatedAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());
        return testEntityManager.persistAndFlush(user);
    }
}

示例 2:Web 层集成测试

java 复制代码
/**
 * Web 层集成测试
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.yml")
@Transactional
class UserControllerIntegrationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    @LocalServerPort
    private int port;
    
    private String baseUrl;
    
    @BeforeEach
    void setUp() {
        baseUrl = "http://localhost:" + port + "/api/users";
    }
    
    /**
     * 测试获取用户列表 API
     */
    @Test
    @DisplayName("测试获取用户列表API")
    void testGetAllUsersApi() {
        // Given - 准备测试数据
        createTestUsers();
        
        // When
        ResponseEntity<UserDto[]> response = restTemplate.getForEntity(baseUrl, UserDto[].class);
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody()).hasSizeGreaterThanOrEqualTo(2);
        
        // 验证响应数据结构
        UserDto firstUser = response.getBody()[0];
        assertThat(firstUser.getId()).isNotNull();
        assertThat(firstUser.getName()).isNotBlank();
        assertThat(firstUser.getEmail()).isNotBlank();
    }
    
    /**
     * 测试根据 ID 获取用户 API
     */
    @Test
    @DisplayName("测试根据ID获取用户API")
    void testGetUserByIdApi() {
        // Given
        User savedUser = createAndSaveUser("API测试用户", "api@example.com");
        
        // When
        ResponseEntity<UserDto> response = restTemplate.getForEntity(
                baseUrl + "/" + savedUser.getId(), 
                UserDto.class
        );
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getId()).isEqualTo(savedUser.getId());
        assertThat(response.getBody().getName()).isEqualTo(savedUser.getName());
        assertThat(response.getBody().getEmail()).isEqualTo(savedUser.getEmail());
    }
    
    /**
     * 测试创建用户 API
     */
    @Test
    @DisplayName("测试创建用户API")
    void testCreateUserApi() {
        // Given
        UserDto newUser = new UserDto();
        newUser.setName("新建用户");
        newUser.setEmail("newuser@example.com");
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<UserDto> request = new HttpEntity<>(newUser, headers);
        
        // When
        ResponseEntity<UserDto> response = restTemplate.postForEntity(baseUrl, request, UserDto.class);
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getId()).isNotNull();
        assertThat(response.getBody().getName()).isEqualTo(newUser.getName());
        assertThat(response.getBody().getEmail()).isEqualTo(newUser.getEmail());
        
        // 验证数据库中确实创建了用户
        Optional<User> savedUser = userRepository.findById(response.getBody().getId());
        assertThat(savedUser).isPresent();
    }
    
    /**
     * 测试更新用户 API
     */
    @Test
    @DisplayName("测试更新用户API")
    void testUpdateUserApi() {
        // Given
        User existingUser = createAndSaveUser("待更新用户", "toupdate@example.com");
        
        UserDto updateData = new UserDto();
        updateData.setName("已更新用户");
        updateData.setEmail("updated@example.com");
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<UserDto> request = new HttpEntity<>(updateData, headers);
        
        // When
        ResponseEntity<UserDto> response = restTemplate.exchange(
                baseUrl + "/" + existingUser.getId(),
                HttpMethod.PUT,
                request,
                UserDto.class
        );
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getId()).isEqualTo(existingUser.getId());
        assertThat(response.getBody().getName()).isEqualTo(updateData.getName());
        assertThat(response.getBody().getEmail()).isEqualTo(updateData.getEmail());
    }
    
    /**
     * 测试删除用户 API
     */
    @Test
    @DisplayName("测试删除用户API")
    void testDeleteUserApi() {
        // Given
        User userToDelete = createAndSaveUser("待删除用户", "todelete@example.com");
        
        // When
        ResponseEntity<Void> response = restTemplate.exchange(
                baseUrl + "/" + userToDelete.getId(),
                HttpMethod.DELETE,
                null,
                Void.class
        );
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
        
        // 验证用户已被删除
        Optional<User> deletedUser = userRepository.findById(userToDelete.getId());
        assertThat(deletedUser).isEmpty();
    }
    
    /**
     * 测试 404 错误处理
     */
    @Test
    @DisplayName("测试用户不存在时的404错误")
    void testUserNotFoundError() {
        // Given
        Long nonExistentId = 99999L;
        
        // When
        ResponseEntity<String> response = restTemplate.getForEntity(
                baseUrl + "/" + nonExistentId, 
                String.class
        );
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
    }
    
    /**
     * 测试数据验证错误
     */
    @Test
    @DisplayName("测试数据验证错误处理")
    void testValidationError() {
        // Given - 无效数据(缺少必填字段)
        UserDto invalidUser = new UserDto();
        // 不设置 name 和 email
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<UserDto> request = new HttpEntity<>(invalidUser, headers);
        
        // When
        ResponseEntity<String> response = restTemplate.postForEntity(baseUrl, request, String.class);
        
        // Then
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
    }
    
    // 辅助方法
    private void createTestUsers() {
        createAndSaveUser("测试用户1", "test1@example.com");
        createAndSaveUser("测试用户2", "test2@example.com");
    }
    
    private User createAndSaveUser(String name, String email) {
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        user.setCreatedAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());
        return userRepository.save(user);
    }
}

示例 3:性能测试

java 复制代码
/**
 * 性能测试
 */
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.yml")
class MapperPerformanceTest {
    
    private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
    
    /**
     * 测试大量数据映射性能
     */
    @Test
    @DisplayName("测试大量数据映射性能")
    @Timeout(value = 5, unit = TimeUnit.SECONDS)  // 5秒超时
    void testLargeDataMappingPerformance() {
        // Given - 创建大量测试数据
        int dataSize = 10000;
        List<User> users = createLargeUserList(dataSize);
        
        // When - 执行映射并测量时间
        long startTime = System.currentTimeMillis();
        List<UserDto> dtos = userMapper.toDtoList(users);
        long endTime = System.currentTimeMillis();
        
        // Then
        assertThat(dtos).hasSize(dataSize);
        
        long executionTime = endTime - startTime;
        System.out.println("映射 " + dataSize + " 个对象耗时: " + executionTime + " ms");
        
        // 性能断言:平均每个对象映射时间应小于 0.1ms
        double avgTimePerObject = (double) executionTime / dataSize;
        assertThat(avgTimePerObject).isLessThan(0.1);
    }
    
    /**
     * 测试内存使用情况
     */
    @Test
    @DisplayName("测试映射过程的内存使用")
    void testMemoryUsage() {
        // Given
        Runtime runtime = Runtime.getRuntime();
        runtime.gc();  // 强制垃圾回收
        
        long memoryBefore = runtime.totalMemory() - runtime.freeMemory();
        
        // When - 执行大量映射操作
        for (int i = 0; i < 1000; i++) {
            List<User> users = createLargeUserList(100);
            List<UserDto> dtos = userMapper.toDtoList(users);
            // 不保持引用,让 GC 可以回收
        }
        
        runtime.gc();  // 强制垃圾回收
        long memoryAfter = runtime.totalMemory() - runtime.freeMemory();
        
        // Then
        long memoryUsed = memoryAfter - memoryBefore;
        System.out.println("内存使用量: " + memoryUsed / 1024 / 1024 + " MB");
        
        // 内存使用应该在合理范围内(小于 100MB)
        assertThat(memoryUsed).isLessThan(100 * 1024 * 1024);
    }
    
    /**
     * 测试并发映射性能
     */
    @Test
    @DisplayName("测试并发映射性能")
    void testConcurrentMappingPerformance() throws InterruptedException {
        // Given
        int threadCount = 10;
        int operationsPerThread = 1000;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);
        AtomicLong totalTime = new AtomicLong(0);
        
        // When
        for (int i = 0; i < threadCount; i++) {
            executor.submit(() -> {
                try {
                    long startTime = System.currentTimeMillis();
                    
                    for (int j = 0; j < operationsPerThread; j++) {
                        User user = createTestUser(j);
                        UserDto dto = userMapper.toDto(user);
                        assertThat(dto).isNotNull();
                    }
                    
                    long endTime = System.currentTimeMillis();
                    totalTime.addAndGet(endTime - startTime);
                    
                } finally {
                    latch.countDown();
                }
            });
        }
        
        // Then
        boolean completed = latch.await(30, TimeUnit.SECONDS);
        assertThat(completed).isTrue();
        
        executor.shutdown();
        
        long avgTime = totalTime.get() / threadCount;
        System.out.println("并发映射平均耗时: " + avgTime + " ms/thread");
        
        // 并发性能应该在合理范围内
        assertThat(avgTime).isLessThan(5000);  // 每个线程平均耗时小于 5 秒
    }
    
    // 辅助方法
    private List<User> createLargeUserList(int size) {
        List<User> users = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            users.add(createTestUser(i));
        }
        return users;
    }
    
    private User createTestUser(int index) {
        User user = new User();
        user.setId((long) index);
        user.setName("用户" + index);
        user.setEmail("user" + index + "@example.com");
        user.setCreatedAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());
        return user;
    }
}

🎬 效果演示:测试执行和报告

运行测试

bash 复制代码
# 运行所有测试
mvn test

# 运行特定测试类
mvn test -Dtest=UserMapperTest

# 运行特定测试方法
mvn test -Dtest=UserMapperTest#testBasicEntityToDto

# 运行集成测试
mvn test -Dtest=*IntegrationTest

# 运行性能测试
mvn test -Dtest=*PerformanceTest

# 生成测试报告
mvn surefire-report:report

# 生成测试覆盖率报告
mvn jacoco:report

测试覆盖率配置

xml 复制代码
<!-- pom.xml -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.7</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>CLASS</element>
                        <limits>
                            <limit>
                                <counter>LINE</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>  <!-- 80% 行覆盖率 -->
                            </limit>
                            <limit>
                                <counter>BRANCH</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.70</minimum>  <!-- 70% 分支覆盖率 -->
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

测试报告示例

yaml 复制代码
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running io.github.nemoob.atlas.mapper.UserMapperTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.234 s - in UserMapperTest
[INFO] Running io.github.nemoob.atlas.mapper.ComplexMappingTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.456 s - in ComplexMappingTest
[INFO] Running io.github.nemoob.atlas.mapper.UserServiceIntegrationTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.123 s - in UserServiceIntegrationTest
[INFO] Running io.github.nemoob.atlas.mapper.MapperPerformanceTest
映射 10000 个对象耗时: 45 ms
并发映射平均耗时: 234 ms/thread
内存使用量: 12 MB
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.567 s - in MapperPerformanceTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

❓ 常见问题

Q1: 如何测试生成的 Mapper 实现类?

A: 有几种方法:

java 复制代码
// 方法1:直接测试接口(推荐)
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);

// 方法2:在 Spring 测试中注入
@Autowired
private UserMapper userMapper;

// 方法3:测试生成的实现类(不推荐)
private final UserMapperImpl userMapperImpl = new UserMapperImpl();

Q2: 如何 Mock 依赖的 Mapper?

A: 使用 Mockito 进行 Mock:

java 复制代码
@ExtendWith(MockitoExtension.class)
class ServiceTest {
    
    @Mock
    private UserMapper userMapper;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void testServiceWithMockedMapper() {
        // Given
        User user = new User();
        UserDto expectedDto = new UserDto();
        
        when(userMapper.toDto(user)).thenReturn(expectedDto);
        
        // When
        UserDto result = userService.convertUser(user);
        
        // Then
        assertThat(result).isEqualTo(expectedDto);
        verify(userMapper).toDto(user);
    }
}

Q3: 如何测试映射的性能?

A: 使用 JMH 或简单的时间测量:

java 复制代码
// 简单性能测试
@Test
@Timeout(value = 1, unit = TimeUnit.SECONDS)
void testMappingPerformance() {
    List<User> users = createLargeDataSet(10000);
    
    long startTime = System.nanoTime();
    List<UserDto> dtos = userMapper.toDtoList(users);
    long endTime = System.nanoTime();
    
    long durationMs = (endTime - startTime) / 1_000_000;
    System.out.println("映射耗时: " + durationMs + " ms");
    
    assertThat(durationMs).isLessThan(500);  // 应在 500ms 内完成
}

// 使用 JMH 进行更精确的性能测试
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class MapperBenchmark {
    
    private UserMapper mapper = Mappers.getMapper(UserMapper.class);
    private User user;
    
    @Setup
    public void setup() {
        user = createTestUser();
    }
    
    @Benchmark
    public UserDto testMapping() {
        return mapper.toDto(user);
    }
}

Q4: 如何测试复杂的嵌套映射?

A: 分层测试和使用测试构建器:

java 复制代码
// 测试构建器模式
public class TestDataBuilder {
    
    public static User.Builder userBuilder() {
        return User.builder()
                .id(1L)
                .name("测试用户")
                .email("test@example.com")
                .createdAt(LocalDateTime.now());
    }
    
    public static Order.Builder orderBuilder() {
        return Order.builder()
                .id(1L)
                .orderNo("TEST001")
                .customer(userBuilder().build())
                .items(Arrays.asList(orderItemBuilder().build()));
    }
}

// 分层测试
@Test
void testNestedMapping() {
    // 先测试简单映射
    User user = TestDataBuilder.userBuilder().build();
    UserDto userDto = userMapper.toDto(user);
    assertThat(userDto).isNotNull();
    
    // 再测试复杂嵌套映射
    Order order = TestDataBuilder.orderBuilder()
            .customer(user)
            .build();
    
    OrderDto orderDto = orderMapper.toDto(order);
    assertThat(orderDto.getCustomer()).isEqualTo(userDto);
}

🎯 本章小结

通过本章学习,你应该掌握了:

  1. 单元测试:Mapper 接口和类型转换器的单元测试编写
  2. 集成测试:Spring Boot 环境下的集成测试设计
  3. 性能测试:映射性能和内存使用的测试方法
  4. 测试策略:测试覆盖率和质量保证的最佳实践

📖 下一步学习

在下一篇教程中,我们将学习:

  • 性能优化技巧和最佳实践
  • 内存管理和缓存策略
  • 生产环境的监控和调优
相关推荐
叽哥2 小时前
Kotlin学习第 7 课:Kotlin 空安全:解决空指针问题的核心机制
android·java·kotlin
guslegend2 小时前
Java面试小册(3)
java
派葛穆2 小时前
Unity-按钮实现场景跳转
java·unity·游戏引擎
弥巷2 小时前
【Android】Viewpager2实现无限轮播图
java
虫小宝2 小时前
返利app排行榜的缓存更新策略:基于过期时间与主动更新的混合方案
java·spring·缓存
SimonKing2 小时前
告别繁琐配置!Retrofit-Spring-Boot-Starter让HTTP调用更优雅
java·后端·程序员
召摇3 小时前
Spring Boot 内置工具类深度指南
java·spring boot
JJJJ_iii3 小时前
【左程云算法09】栈的入门题目-最小栈
java·开发语言·数据结构·算法·时间复杂度
所愿ღ3 小时前
JavaWeb-Session和ServletContext
java·笔记·servlet