当前位置: 首页 > news >正文

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=FALSEdriver-class-name: org.h2.Driverusername: sapassword: # JPA 测试配置jpa:hibernate:ddl-auto: create-dropshow-sql: trueproperties:hibernate:format_sql: truedialect: org.hibernate.dialect.H2Dialect# Atlas Mapper 测试配置
atlas:mapper:enabled: trueverbose: true                    # 测试环境启用详细日志show-generated-code: true        # 显示生成代码便于调试performance-monitoring: false    # 测试环境关闭性能监控# 日志配置
logging:level:io.github.nemoob.atlas.mapper: DEBUGorg.springframework.test: DEBUGorg.hibernate.SQL: DEBUGorg.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() {// GivenUserDto dto = createTestUserDto();// WhenUser entity = userMapper.toEntity(dto);// ThenassertThat(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;// WhenUserDto dto = userMapper.toDto(nullUser);// ThenassertThat(dto).isNull();// Given - 部分字段为 null 的对象User userWithNulls = new User();userWithNulls.setId(1L);userWithNulls.setName(null);  // null 字段userWithNulls.setEmail("test@example.com");// WhenUserDto dtoWithNulls = userMapper.toDto(userWithNulls);// ThenassertThat(dtoWithNulls).isNotNull();assertThat(dtoWithNulls.getId()).isEqualTo(1L);assertThat(dtoWithNulls.getName()).isNull();assertThat(dtoWithNulls.getEmail()).isEqualTo("test@example.com");}/*** 测试集合映射*/@Test@DisplayName("测试用户列表的映射")void testListMapping() {// GivenList<User> users = Arrays.asList(createTestUser(1L, "张三", "zhangsan@example.com"),createTestUser(2L, "李四", "lisi@example.com"),createTestUser(3L, "王五", "wangwu@example.com"));// WhenList<UserDto> dtos = userMapper.toDtoList(users);// ThenassertThat(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;// WhenList<UserDto> nullResult = userMapper.toDtoList(nullList);// ThenassertThat(nullResult).isNull();// Given - 空列表List<User> emptyList = Collections.emptyList();// WhenList<UserDto> emptyResult = userMapper.toDtoList(emptyList);// ThenassertThat(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() {// GivenOrder order = createComplexOrder();// WhenOrderDto dto = orderMapper.toDto(order);// ThenassertThat(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() {// GivenOrder order = createOrderWithItems();// WhenOrderDto dto = orderMapper.toDto(order);// ThenassertThat(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() {// GivenOrder order = createOrderWithCalculatedFields();// WhenOrderDto 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);// ThenassertThat(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("科技园南区");// WhenString result = converter.addressToString(fullAddress);// ThenassertThat(result).isEqualTo("广东省深圳市南山区科技园南区");// Given - 部分地址Address partialAddress = new Address();partialAddress.setProvince("北京市");partialAddress.setCity("北京市");// WhenString partialResult = converter.addressToString(partialAddress);// ThenassertThat(partialResult).isEqualTo("北京市北京市");// Given - null 地址String nullResult = converter.addressToString(null);// ThenassertThat(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 {@Autowiredprivate UserService userService;@Autowiredprivate UserRepository userRepository;@Autowiredprivate UserMapper userMapper;@Autowiredprivate TestEntityManager testEntityManager;/*** 测试用户创建的完整流程*/@Test@DisplayName("测试用户创建的完整流程")void testCreateUserCompleteFlow() {// GivenUserDto inputDto = new UserDto();inputDto.setName("集成测试用户");inputDto.setEmail("integration@example.com");// WhenUserDto resultDto = userService.createUser(inputDto);// ThenassertThat(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");// WhenUserDto resultDto = userService.updateUser(savedUser.getId(), updateDto);// ThenassertThat(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"));// WhenList<UserDto> dtos = userService.convertUsers(users);// ThenassertThat(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() {// GivenLong nonExistentId = 99999L;// When & ThenassertThatThrownBy(() -> 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 {@Autowiredprivate TestRestTemplate restTemplate;@Autowiredprivate UserRepository userRepository;@LocalServerPortprivate int port;private String baseUrl;@BeforeEachvoid setUp() {baseUrl = "http://localhost:" + port + "/api/users";}/*** 测试获取用户列表 API*/@Test@DisplayName("测试获取用户列表API")void testGetAllUsersApi() {// Given - 准备测试数据createTestUsers();// WhenResponseEntity<UserDto[]> response = restTemplate.getForEntity(baseUrl, UserDto[].class);// ThenassertThat(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() {// GivenUser savedUser = createAndSaveUser("API测试用户", "api@example.com");// WhenResponseEntity<UserDto> response = restTemplate.getForEntity(baseUrl + "/" + savedUser.getId(), UserDto.class);// ThenassertThat(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() {// GivenUserDto 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);// WhenResponseEntity<UserDto> response = restTemplate.postForEntity(baseUrl, request, UserDto.class);// ThenassertThat(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() {// GivenUser 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);// WhenResponseEntity<UserDto> response = restTemplate.exchange(baseUrl + "/" + existingUser.getId(),HttpMethod.PUT,request,UserDto.class);// ThenassertThat(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() {// GivenUser userToDelete = createAndSaveUser("待删除用户", "todelete@example.com");// WhenResponseEntity<Void> response = restTemplate.exchange(baseUrl + "/" + userToDelete.getId(),HttpMethod.DELETE,null,Void.class);// ThenassertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);// 验证用户已被删除Optional<User> deletedUser = userRepository.findById(userToDelete.getId());assertThat(deletedUser).isEmpty();}/*** 测试 404 错误处理*/@Test@DisplayName("测试用户不存在时的404错误")void testUserNotFoundError() {// GivenLong nonExistentId = 99999L;// WhenResponseEntity<String> response = restTemplate.getForEntity(baseUrl + "/" + nonExistentId, String.class);// ThenassertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);}/*** 测试数据验证错误*/@Test@DisplayName("测试数据验证错误处理")void testValidationError() {// Given - 无效数据(缺少必填字段)UserDto invalidUser = new UserDto();// 不设置 name 和 emailHttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);HttpEntity<UserDto> request = new HttpEntity<>(invalidUser, headers);// WhenResponseEntity<String> response = restTemplate.postForEntity(baseUrl, request, String.class);// ThenassertThat(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();// ThenassertThat(dtos).hasSize(dataSize);long executionTime = endTime - startTime;System.out.println("映射 " + dataSize + " 个对象耗时: " + executionTime + " ms");// 性能断言:平均每个对象映射时间应小于 0.1msdouble avgTimePerObject = (double) executionTime / dataSize;assertThat(avgTimePerObject).isLessThan(0.1);}/*** 测试内存使用情况*/@Test@DisplayName("测试映射过程的内存使用")void testMemoryUsage() {// GivenRuntime 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();// Thenlong memoryUsed = memoryAfter - memoryBefore;System.out.println("内存使用量: " + memoryUsed / 1024 / 1024 + " MB");// 内存使用应该在合理范围内(小于 100MB)assertThat(memoryUsed).isLessThan(100 * 1024 * 1024);}/*** 测试并发映射性能*/@Test@DisplayName("测试并发映射性能")void testConcurrentMappingPerformance() throws InterruptedException {// Givenint threadCount = 10;int operationsPerThread = 1000;ExecutorService executor = Executors.newFixedThreadPool(threadCount);CountDownLatch latch = new CountDownLatch(threadCount);AtomicLong totalTime = new AtomicLong(0);// Whenfor (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();}});}// Thenboolean 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 {@Mockprivate UserMapper userMapper;@InjectMocksprivate UserService userService;@Testvoid testServiceWithMockedMapper() {// GivenUser user = new User();UserDto expectedDto = new UserDto();when(userMapper.toDto(user)).thenReturn(expectedDto);// WhenUserDto result = userService.convertUser(user);// ThenassertThat(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;@Setuppublic void setup() {user = createTestUser();}@Benchmarkpublic 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. 测试策略:测试覆盖率和质量保证的最佳实践
http://www.dtcms.com/a/409927.html

相关文章:

  • 众智FlagOS 1.5发布:统一开源大模型系统软件栈,更全面、AI赋能更高效
  • 理解 mvcc
  • 【网络编程】TCP 粘包处理:手动序列化反序列化与报头封装的完整方案
  • 数据库MVCC
  • 如何用AI工具开发一个轻量化CRM系统(七):AI生成pytest测试脚本
  • qData:一站式开源数据中台
  • 国外中文网站排行在线图片编辑网站源码
  • [数据结构]优先级队列
  • ARM内部寄存器
  • Laravel + UniApp AES加密/解密
  • 5G开户时切片配置参数详解
  • 面向新质生产力,职业院校“人工智能”课程教学解决方案
  • wap网站如何做福建外贸网站
  • ElasticSearch-提高篇
  • 第6篇、Flask 表单处理与用户认证完全指南:从零到实战
  • Visual Studio 2013 Update 4 中文版安装步骤(带TFS支持)附安装包​
  • 珠海 网站建设注册安全工程师题库
  • 上手 cpp-httplib:轻量级 C++ HTTP 库的安装与实战指南
  • 突破文档型数据库迁移困境:金仓多模方案破解电子证照系统国产化难题
  • 网站手机客户端开发wordpress制造商单页
  • Net 》》C# 》》try finally 执行顺序
  • 在 Unity 项目中使用 FFMpeg 进行音频转码(WAV 转 MP3)
  • 使用Java将Word文件转换为PNG图片
  • 如何用Fail2ban保护Linux服务器?防止SSH暴力破解教程
  • 开源 C# 快速开发(五)自定义控件--仪表盘
  • 华为FreeClip 2耳夹耳机:让「戴着不摘」成为新的使用习惯
  • 算法继续刷起-2025年09月26日
  • AI笔记在学习与工作中的高效运用
  • QML学习笔记(十四)QML的自定义模块
  • ubuntu一键安装vscode: 使用官方 APT 仓库