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

学习目标

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

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

概念讲解:测试策略架构

测试金字塔

在这里插入图片描述

测试类型和范围

端到端测试
集成测试
单元测试
API测试
业务流程测试
性能测试
兼容性测试
Spring容器测试
数据库集成测试
Service层测试
Controller层测试
Mapper接口测试
类型转换器测试
映射规则测试
边界条件测试

实现步骤:单元测试详解

步骤 1:测试环境搭建

添加测试依赖
<!-- 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>
测试配置文件
# 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 测试
/**
* 用户映射器单元测试
*/
@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;
            }
            }
复杂映射测试
/**
* 复杂映射场景测试
*/
@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:类型转换器测试

/**
* 自定义类型转换器测试
*/
@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 集成测试

/**
* 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 层集成测试

/**
* 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:性能测试

/**
* 性能测试
*/
@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;
            }
            }

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

运行测试

# 运行所有测试
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

测试覆盖率配置

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

测试报告示例

[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: 有几种方法:

// 方法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:

@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 或简单的时间测量:

// 简单性能测试
@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: 分层测试和使用测试构建器:

// 测试构建器模式
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. 测试策略:测试覆盖率和质量保证的最佳实践
posted @ 2025-09-28 11:28  yxysuanfa  阅读(6)  评论(0)    收藏  举报