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

news/2025/9/28 11:35:05/文章来源:https://www.cnblogs.com/yxysuanfa/p/19116353

学习目标

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

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

概念讲解:测试策略架构

测试金字塔

在这里插入图片描述

测试类型和范围

实现步骤:单元测试详解

步骤 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"));// 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() {
// 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");// 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 {
@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);// 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 {
@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;@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.mzph.cn/news/920587.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python第三天

作业: 题目描述 模拟一个数字密码锁的工作过程,规则如下: 1. 初始密码为四位数:1234 2. 用户有3次输入密码的机会 3. 每次输入后: - 若密码正确,提示"密码正确,锁已打开"并结束程序 - 若密码错误,提…

iOS Xcode16 中删除描述文件 Provisioning Profiles

在访问中,前往文件夹:~/Library/Developer/Xcode/UserData/Provisioning Profiles/ 注:适用于高版本Xcode,低版本的文件夹是:~/Library/MobileDevice/Provisioning Profiles​

深圳全网营销型网站昆明seo网站排名

文章目录 为什么需要时间复杂度分析&#xff1f;一、大O表示法&#xff1a;复杂度的语言1.1 什么是大O&#xff1f;1.2 常见复杂度速查表 二、实战分析&#xff1a;解剖C语言代码2.1 循环结构的三重境界单层循环&#xff1a;线性时间双重循环&#xff1a;平方时间动态边界循环&…

git仓库管理memo

git clone url git checkout branch git pull <远程仓库名默认则填origin> <远程分支名>:<本地分支名> git checkout -b exp/new 创建分支并切换查看当前改动状态 (可选,但好习惯) git status 添加…

企业网站策划书范文3000字网站显示500错误怎么解决方法

&#xfeff;&#xfeff;Android CustomShapeImageView对图片进行各种样式裁剪&#xff1a;圆形、星形、心形、花瓣形等Android CustomShapeImageView是github上一个第三方开源的对图片进行各种样式裁剪的库&#xff0c;其要实现的功能如图所示&#xff1a;Android CustomShap…

网站开发时ie11的兼容挂机赚一小时75元

架构师进阶有一块很重要的内容&#xff0c;就是需要掌握大数据的架构设计&#xff0c;主要涵括&#xff1a; MySQL等关系式数据库&#xff0c;需要掌握数据库的索引、慢SQL、以及长事务的优化等。 需要掌握非关系式数据库&#xff08;NoSQL&#xff09;的选型&#xff0c;以及…

全国主要城市温度舒适度榜:谁在天堂,谁在蒸笼

全国主要城市温度舒适度榜:谁在天堂,谁在蒸笼你是愿意做三亚的“向日葵族”,在热带季风里野蛮生长?还是当昆明的“佛系仙人”,在四季恒温中参悟人生?又或者,你和长沙人一样,用麻辣江湖味对冲高温暴击? 一起来…

零基础新手小白快速了解掌握服务集群与自动化运维(七)Nginx模块--Nginx反向代理与缓存功能(二) - 实践

零基础新手小白快速了解掌握服务集群与自动化运维(七)Nginx模块--Nginx反向代理与缓存功能(二) - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display:…

深入解析:Vue3中文本与图片一起垂直居中

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

建设网站个人网上银行今天国际新闻最新消息10条

Response 对象&#xff0c;派生自HttpResponse 类&#xff0c;该类封装来自 ASP.NET 操作的 HTTP 响应信息。存在于System.Web命名空间下。 注&#xff1a;MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型就是设定某种扩展名的文件用一种应用程序来打开的…

电桥采集模块 24位ADC+128倍可调增益 高精度测量支持多接口输出

电桥采集模块 24位ADC+128倍可调增益 高精度测量支持多接口输出BRGxxx系列模块是一款专为惠斯通电桥测量设计的高分辨率数据采集模块,采用24位ADC同步测量激励源与电桥输出信号,并提供1~128倍可调增益放大,确保高精…

ubuntu 系统启动服务及服务依赖

ubuntu 系统启动服务及服务依赖[Unit] Description=QNet Script After=NetworkManager.serviceo[Service] ExecStart=/usr/bin/bash -c source /home/qpanda/myenv/bin/activate && python3 /home/qpanda/qnet…

Jira停售Data Center尘埃落定!中国企业迁移需落实的6大关键项目管理工具清单

随着Jira Data Center版本停售,中国企业亟需寻找替代方案以确保项目管理连续性。本文将为您精准筛选6类必备工具,覆盖从需求管理到合规审计的全流程:需求管理与优先级规划工具、敏捷开发与任务协作平台、DevOps全链…

实用指南:学习React-17-useMemo

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

集团网站制作公司用python做网站优点

来源&#xff1a;经济日报-中国经济网记者/梁 睿米磊建议创投和保荐机构从五个方面入手筛选“硬科技”企业&#xff1a;“围绕落实国家战略规划确定的科技发展方向或承担具体攻关任务的企业&#xff1b;拥有关键核心技术和先进技术的企业&#xff1b;科技创新和转化应用能力突出…

Gitee崛起:中国开发者迎来本土化研发平台新纪元

Gitee崛起:中国开发者迎来本土化研发平台新纪元 腾讯CODING DevOps服务的战略调整正在引发国内开发者生态的深刻变革。随着这一主流研发平台的逐步退出,中国软件产业正面临关键技术栈迁移的关键拐点。在这场转型浪潮…

湖北做网站教程哪家好上海知名的网站建设

在爱情中&#xff0c;不是每个男生都是幸运的&#xff0c;也不是每次的表白都是如你所愿的。有成功总有失败&#xff0c;成功是喜悦的&#xff0c;但是失败却是痛苦的。不过有的时候男生和女生表白以后&#xff0c;女生对男生说了这样的一段&#xff0c;其实我希望以后我们还是…

赣州做网站多少钱暴雪公司

解决神经网络过拟合问题—Dropout方法一、what is Dropout&#xff1f;如何实现&#xff1f;二、使用和不使用Dropout的训练结果对比一、what is Dropout&#xff1f;如何实现&#xff1f; 如果网络模型复杂&#xff0c;L2范数权值衰减方法就难以对付过拟合。这种情况下&#…

中国建设银行网站官网下载建设集团英文缩写

支付宝当面付官方接口文档&#xff1a;https://docs.open.alipay.com/194/105170/ 在弄清楚如何计算优惠之前先了解下相关金额参数&#xff1a; 1、请求中金额参数total_amount&#xff1a;订单总金额&#xff0c;订单总金额&#xff0c;单位为元&#xff0c;精确到小数点后两位…

关键领域软件研发知识管理的范式革命:从静态文档到智能图谱的跃迁

关键领域软件研发知识管理的范式革命:从静态文档到智能图谱的跃迁 在航空航天、金融科技和工业控制等关键领域,一场关于软件研发知识管理的静默革命正在上演。传统文档管理方式已无法满足现代软件工程对协作效率和知…