🎯 学习目标
通过本篇教程,你将学会:
- 掌握 Atlas Mapper 的单元测试编写方法
- 学会使用 Mock 和测试数据进行测试
- 理解集成测试的设计和实现
- 掌握测试覆盖率分析和质量保证
📋 概念讲解:测试策略架构
测试金字塔

测试类型和范围
flowchart LR
subgraph "单元测试"
A1[Mapper接口测试]
A2[类型转换器测试]
A3[映射规则测试]
A4[边界条件测试]
end
subgraph "集成测试"
B1[Spring容器测试]
B2[数据库集成测试]
B3[Service层测试]
B4[Controller层测试]
end
subgraph "端到端测试"
C1[API测试]
C2[业务流程测试]
C3[性能测试]
C4[兼容性测试]
end
A1 --> B1
A2 --> B2
A3 --> B3
A4 --> B4
B1 --> C1
B2 --> C2
B3 --> C3
B4 --> C4
style A1 fill:#c8e6c9
style B1 fill:#e8f5e8
style C1 fill:#fff3e0
🔧 实现步骤:单元测试详解
步骤 1:测试环境搭建
添加测试依赖
xml
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Atlas Mapper Test -->
<dependency>
<groupId>io.github.nemoob</groupId>
<artifactId>atlas-mapper-test</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
<!-- Testcontainers (可选,用于集成测试) -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
<!-- MockWebServer (可选,用于外部API测试) -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
测试配置文件
yaml
# src/test/resources/application-test.yml
spring:
profiles:
active: test
# 测试数据源配置
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
# JPA 测试配置
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.H2Dialect
# Atlas Mapper 测试配置
atlas:
mapper:
enabled: true
verbose: true # 测试环境启用详细日志
show-generated-code: true # 显示生成代码便于调试
performance-monitoring: false # 测试环境关闭性能监控
# 日志配置
logging:
level:
io.github.nemoob.atlas.mapper: DEBUG
org.springframework.test: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
步骤 2:Mapper 单元测试
基础 Mapper 测试
java
/**
* 用户映射器单元测试
*/
@ExtendWith(MockitoExtension.class)
class UserMapperTest {
// 🔥 使用 Mappers.getMapper() 获取映射器实例
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
/**
* 测试基本映射功能
*/
@Test
@DisplayName("测试用户实体到DTO的基本映射")
void testBasicEntityToDto() {
// Given - 准备测试数据
User user = createTestUser();
// When - 执行映射
UserDto dto = userMapper.toDto(user);
// Then - 验证结果
assertThat(dto).isNotNull();
assertThat(dto.getId()).isEqualTo(user.getId());
assertThat(dto.getName()).isEqualTo(user.getName());
assertThat(dto.getEmail()).isEqualTo(user.getEmail());
// 🔥 使用 AssertJ 的软断言
assertThat(dto)
.extracting("id", "name", "email")
.containsExactly(user.getId(), user.getName(), user.getEmail());
}
/**
* 测试反向映射
*/
@Test
@DisplayName("测试DTO到用户实体的反向映射")
void testBasicDtoToEntity() {
// Given
UserDto dto = createTestUserDto();
// When
User entity = userMapper.toEntity(dto);
// Then
assertThat(entity).isNotNull();
assertThat(entity.getId()).isEqualTo(dto.getId());
assertThat(entity.getName()).isEqualTo(dto.getName());
assertThat(entity.getEmail()).isEqualTo(dto.getEmail());
}
/**
* 测试空值处理
*/
@Test
@DisplayName("测试空值和null值的处理")
void testNullValueHandling() {
// Given - null 对象
User nullUser = null;
// When
UserDto dto = userMapper.toDto(nullUser);
// Then
assertThat(dto).isNull();
// Given - 部分字段为 null 的对象
User userWithNulls = new User();
userWithNulls.setId(1L);
userWithNulls.setName(null); // null 字段
userWithNulls.setEmail("test@example.com");
// When
UserDto dtoWithNulls = userMapper.toDto(userWithNulls);
// Then
assertThat(dtoWithNulls).isNotNull();
assertThat(dtoWithNulls.getId()).isEqualTo(1L);
assertThat(dtoWithNulls.getName()).isNull();
assertThat(dtoWithNulls.getEmail()).isEqualTo("test@example.com");
}
/**
* 测试集合映射
*/
@Test
@DisplayName("测试用户列表的映射")
void testListMapping() {
// Given
List<User> users = Arrays.asList(
createTestUser(1L, "张三", "zhangsan@example.com"),
createTestUser(2L, "李四", "lisi@example.com"),
createTestUser(3L, "王五", "wangwu@example.com")
);
// When
List<UserDto> dtos = userMapper.toDtoList(users);
// Then
assertThat(dtos).hasSize(3);
assertThat(dtos)
.extracting("name")
.containsExactly("张三", "李四", "王五");
// 验证每个元素的映射
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
UserDto dto = dtos.get(i);
assertThat(dto.getId()).isEqualTo(user.getId());
assertThat(dto.getName()).isEqualTo(user.getName());
assertThat(dto.getEmail()).isEqualTo(user.getEmail());
}
}
/**
* 测试空集合映射
*/
@Test
@DisplayName("测试空集合和null集合的映射")
void testEmptyAndNullListMapping() {
// Given - null 列表
List<User> nullList = null;
// When
List<UserDto> nullResult = userMapper.toDtoList(nullList);
// Then
assertThat(nullResult).isNull();
// Given - 空列表
List<User> emptyList = Collections.emptyList();
// When
List<UserDto> emptyResult = userMapper.toDtoList(emptyList);
// Then
assertThat(emptyResult).isEmpty();
}
// 辅助方法
private User createTestUser() {
return createTestUser(1L, "测试用户", "test@example.com");
}
private User createTestUser(Long id, String name, String email) {
User user = new User();
user.setId(id);
user.setName(name);
user.setEmail(email);
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
return user;
}
private UserDto createTestUserDto() {
UserDto dto = new UserDto();
dto.setId(1L);
dto.setName("测试用户");
dto.setEmail("test@example.com");
return dto;
}
}
复杂映射测试
java
/**
* 复杂映射场景测试
*/
@ExtendWith(MockitoExtension.class)
class ComplexMappingTest {
private final OrderMapper orderMapper = Mappers.getMapper(OrderMapper.class);
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
/**
* 测试嵌套对象映射
*/
@Test
@DisplayName("测试订单嵌套对象映射")
void testNestedObjectMapping() {
// Given
Order order = createComplexOrder();
// When
OrderDto dto = orderMapper.toDto(order);
// Then
assertThat(dto).isNotNull();
assertThat(dto.getId()).isEqualTo(order.getId());
assertThat(dto.getOrderNo()).isEqualTo(order.getOrderNo());
// 验证嵌套的客户信息
assertThat(dto.getCustomer()).isNotNull();
assertThat(dto.getCustomer().getId()).isEqualTo(order.getCustomer().getId());
assertThat(dto.getCustomer().getName()).isEqualTo(order.getCustomer().getName());
// 验证嵌套的地址信息
assertThat(dto.getCustomer().getAddress()).isNotNull();
assertThat(dto.getCustomer().getAddress().getProvince())
.isEqualTo(order.getCustomer().getAddress().getProvince());
}
/**
* 测试集合嵌套映射
*/
@Test
@DisplayName("测试订单项集合映射")
void testNestedCollectionMapping() {
// Given
Order order = createOrderWithItems();
// When
OrderDto dto = orderMapper.toDto(order);
// Then
assertThat(dto.getItems()).hasSize(order.getItems().size());
for (int i = 0; i < order.getItems().size(); i++) {
OrderItem item = order.getItems().get(i);
OrderItemDto itemDto = dto.getItems().get(i);
assertThat(itemDto.getId()).isEqualTo(item.getId());
assertThat(itemDto.getQuantity()).isEqualTo(item.getQuantity());
// 验证嵌套的产品信息
assertThat(itemDto.getProduct()).isNotNull();
assertThat(itemDto.getProduct().getId()).isEqualTo(item.getProduct().getId());
}
}
/**
* 测试自定义映射方法
*/
@Test
@DisplayName("测试自定义映射方法和表达式")
void testCustomMappingMethods() {
// Given
Order order = createOrderWithCalculatedFields();
// When
OrderDto dto = orderMapper.toDto(order);
// Then
// 验证计算字段
assertThat(dto.getTotalAmount()).isNotNull();
assertThat(dto.getTotalItems()).isGreaterThan(0);
// 验证格式化字段
assertThat(dto.getCreatedAt()).matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}");
// 验证自定义转换
if (order.getStatus() != null) {
assertThat(dto.getStatusDesc()).isNotBlank();
}
}
/**
* 测试循环引用处理
*/
@Test
@DisplayName("测试循环引用的处理")
void testCircularReferenceHandling() {
// Given - 创建有循环引用的对象
Category parent = new Category();
parent.setId(1L);
parent.setName("父分类");
Category child = new Category();
child.setId(2L);
child.setName("子分类");
child.setParent(parent);
parent.setChildren(Arrays.asList(child));
// When - 使用浅层映射避免循环引用
CategoryMapper categoryMapper = Mappers.getMapper(CategoryMapper.class);
CategoryDto dto = categoryMapper.toShallowDto(parent);
// Then
assertThat(dto).isNotNull();
assertThat(dto.getId()).isEqualTo(parent.getId());
assertThat(dto.getName()).isEqualTo(parent.getName());
// 验证循环引用字段被忽略
assertThat(dto.getParent()).isNull();
assertThat(dto.getChildren()).isNull();
}
// 辅助方法
private Order createComplexOrder() {
// 创建地址
Address address = new Address();
address.setProvince("广东省");
address.setCity("深圳市");
address.setDistrict("南山区");
address.setDetail("科技园");
// 创建客户
UserWithAddress customer = new UserWithAddress();
customer.setId(1L);
customer.setName("张三");
customer.setEmail("zhangsan@example.com");
customer.setAddress(address);
// 创建订单
Order order = new Order();
order.setId(1001L);
order.setOrderNo("ORD20250109001");
order.setCustomer(customer);
order.setCreatedAt(LocalDateTime.now());
order.setStatus(1);
return order;
}
private Order createOrderWithItems() {
Order order = createComplexOrder();
// 创建产品
Product product1 = new Product();
product1.setId(1L);
product1.setName("iPhone 15");
product1.setPrice(new BigDecimal("8999.00"));
Product product2 = new Product();
product2.setId(2L);
product2.setName("保护壳");
product2.setPrice(new BigDecimal("99.00"));
// 创建订单项
OrderItem item1 = new OrderItem();
item1.setId(1L);
item1.setProduct(product1);
item1.setQuantity(1);
item1.setUnitPrice(product1.getPrice());
OrderItem item2 = new OrderItem();
item2.setId(2L);
item2.setProduct(product2);
item2.setQuantity(2);
item2.setUnitPrice(product2.getPrice());
order.setItems(Arrays.asList(item1, item2));
return order;
}
private Order createOrderWithCalculatedFields() {
Order order = createOrderWithItems();
order.setMetadata(Map.of("source", "mobile", "channel", "app"));
order.setTags(Set.of("urgent", "vip"));
return order;
}
}
步骤 3:类型转换器测试
java
/**
* 自定义类型转换器测试
*/
@ExtendWith(MockitoExtension.class)
class CustomTypeConverterTest {
private final CustomTypeConverter converter = new CustomTypeConverter();
/**
* 测试状态码转换
*/
@Test
@DisplayName("测试状态码到描述的转换")
void testStatusCodeToDescription() {
// 测试正常值
assertThat(converter.statusCodeToDescription(0)).isEqualTo("待处理");
assertThat(converter.statusCodeToDescription(1)).isEqualTo("处理中");
assertThat(converter.statusCodeToDescription(2)).isEqualTo("已完成");
assertThat(converter.statusCodeToDescription(3)).isEqualTo("已取消");
// 测试边界值
assertThat(converter.statusCodeToDescription(null)).isEqualTo("未知状态");
assertThat(converter.statusCodeToDescription(-1)).isEqualTo("未知状态");
assertThat(converter.statusCodeToDescription(999)).isEqualTo("未知状态");
}
/**
* 测试反向转换
*/
@Test
@DisplayName("测试描述到状态码的反向转换")
void testDescriptionToStatusCode() {
// 测试正常值
assertThat(converter.descriptionToStatusCode("待处理")).isEqualTo(0);
assertThat(converter.descriptionToStatusCode("处理中")).isEqualTo(1);
assertThat(converter.descriptionToStatusCode("已完成")).isEqualTo(2);
assertThat(converter.descriptionToStatusCode("已取消")).isEqualTo(3);
// 测试边界值
assertThat(converter.descriptionToStatusCode(null)).isEqualTo(0);
assertThat(converter.descriptionToStatusCode("")).isEqualTo(0);
assertThat(converter.descriptionToStatusCode("未知状态")).isEqualTo(0);
}
/**
* 测试金额转换
*/
@Test
@DisplayName("测试分到元的金额转换")
void testCentToYuan() {
// 测试正常值
assertThat(converter.centToYuan(100L)).isEqualTo("¥1.00");
assertThat(converter.centToYuan(12345L)).isEqualTo("¥123.45");
assertThat(converter.centToYuan(999999L)).isEqualTo("¥9999.99");
// 测试边界值
assertThat(converter.centToYuan(0L)).isEqualTo("¥0.00");
assertThat(converter.centToYuan(null)).isEqualTo("¥0.00");
// 测试精度
assertThat(converter.centToYuan(1L)).isEqualTo("¥0.01");
assertThat(converter.centToYuan(99L)).isEqualTo("¥0.99");
}
/**
* 测试地址转换
*/
@Test
@DisplayName("测试地址对象到字符串的转换")
void testAddressToString() {
// Given - 完整地址
Address fullAddress = new Address();
fullAddress.setProvince("广东省");
fullAddress.setCity("深圳市");
fullAddress.setDistrict("南山区");
fullAddress.setDetail("科技园南区");
// When
String result = converter.addressToString(fullAddress);
// Then
assertThat(result).isEqualTo("广东省深圳市南山区科技园南区");
// Given - 部分地址
Address partialAddress = new Address();
partialAddress.setProvince("北京市");
partialAddress.setCity("北京市");
// When
String partialResult = converter.addressToString(partialAddress);
// Then
assertThat(partialResult).isEqualTo("北京市北京市");
// Given - null 地址
String nullResult = converter.addressToString(null);
// Then
assertThat(nullResult).isEmpty();
}
/**
* 测试时间戳转换
*/
@Test
@DisplayName("测试时间戳到相对时间的转换")
void testTimestampToRelativeTime() {
long now = System.currentTimeMillis();
// 测试不同时间间隔
assertThat(converter.timestampToRelativeTime(now - 30 * 1000)).isEqualTo("刚刚"); // 30秒前
assertThat(converter.timestampToRelativeTime(now - 5 * 60 * 1000)).isEqualTo("5分钟前"); // 5分钟前
assertThat(converter.timestampToRelativeTime(now - 2 * 60 * 60 * 1000)).isEqualTo("2小时前"); // 2小时前
assertThat(converter.timestampToRelativeTime(now - 3 * 24 * 60 * 60 * 1000)).isEqualTo("3天前"); // 3天前
// 测试边界值
assertThat(converter.timestampToRelativeTime(null)).isEqualTo("未知时间");
}
}
💻 示例代码:集成测试详解
示例 1:Spring Boot 集成测试
java
/**
* Spring Boot 集成测试
*/
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.yml")
@Transactional
@Rollback
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Autowired
private UserMapper userMapper;
@Autowired
private TestEntityManager testEntityManager;
/**
* 测试用户创建的完整流程
*/
@Test
@DisplayName("测试用户创建的完整流程")
void testCreateUserCompleteFlow() {
// Given
UserDto inputDto = new UserDto();
inputDto.setName("集成测试用户");
inputDto.setEmail("integration@example.com");
// When
UserDto resultDto = userService.createUser(inputDto);
// Then
assertThat(resultDto).isNotNull();
assertThat(resultDto.getId()).isNotNull();
assertThat(resultDto.getName()).isEqualTo(inputDto.getName());
assertThat(resultDto.getEmail()).isEqualTo(inputDto.getEmail());
// 验证数据库中的数据
Optional<User> savedUser = userRepository.findById(resultDto.getId());
assertThat(savedUser).isPresent();
assertThat(savedUser.get().getName()).isEqualTo(inputDto.getName());
assertThat(savedUser.get().getEmail()).isEqualTo(inputDto.getEmail());
assertThat(savedUser.get().getCreatedAt()).isNotNull();
assertThat(savedUser.get().getUpdatedAt()).isNotNull();
}
/**
* 测试用户更新流程
*/
@Test
@DisplayName("测试用户更新流程")
void testUpdateUserFlow() {
// Given - 先创建一个用户
User existingUser = new User();
existingUser.setName("原始用户");
existingUser.setEmail("original@example.com");
existingUser.setCreatedAt(LocalDateTime.now());
existingUser.setUpdatedAt(LocalDateTime.now());
User savedUser = testEntityManager.persistAndFlush(existingUser);
// 准备更新数据
UserDto updateDto = new UserDto();
updateDto.setName("更新后用户");
updateDto.setEmail("updated@example.com");
// When
UserDto resultDto = userService.updateUser(savedUser.getId(), updateDto);
// Then
assertThat(resultDto).isNotNull();
assertThat(resultDto.getId()).isEqualTo(savedUser.getId());
assertThat(resultDto.getName()).isEqualTo(updateDto.getName());
assertThat(resultDto.getEmail()).isEqualTo(updateDto.getEmail());
// 验证数据库中的数据
testEntityManager.clear(); // 清除一级缓存
User updatedUser = testEntityManager.find(User.class, savedUser.getId());
assertThat(updatedUser.getName()).isEqualTo(updateDto.getName());
assertThat(updatedUser.getEmail()).isEqualTo(updateDto.getEmail());
assertThat(updatedUser.getUpdatedAt()).isAfter(updatedUser.getCreatedAt());
}
/**
* 测试批量操作
*/
@Test
@DisplayName("测试批量用户转换")
void testBatchUserConversion() {
// Given - 创建多个用户
List<User> users = Arrays.asList(
createAndSaveUser("用户1", "user1@example.com"),
createAndSaveUser("用户2", "user2@example.com"),
createAndSaveUser("用户3", "user3@example.com")
);
// When
List<UserDto> dtos = userService.convertUsers(users);
// Then
assertThat(dtos).hasSize(3);
for (int i = 0; i < users.size(); i++) {
User user = users.get(i);
UserDto dto = dtos.get(i);
assertThat(dto.getId()).isEqualTo(user.getId());
assertThat(dto.getName()).isEqualTo(user.getName());
assertThat(dto.getEmail()).isEqualTo(user.getEmail());
}
}
/**
* 测试异常情况
*/
@Test
@DisplayName("测试用户不存在的异常情况")
void testUserNotFoundExceptionHandling() {
// Given
Long nonExistentId = 99999L;
// When & Then
assertThatThrownBy(() -> userService.getUserById(nonExistentId))
.isInstanceOf(EntityNotFoundException.class)
.hasMessageContaining("用户不存在: " + nonExistentId);
}
// 辅助方法
private User createAndSaveUser(String name, String email) {
User user = new User();
user.setName(name);
user.setEmail(email);
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
return testEntityManager.persistAndFlush(user);
}
}
示例 2:Web 层集成测试
java
/**
* Web 层集成测试
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.yml")
@Transactional
class UserControllerIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@LocalServerPort
private int port;
private String baseUrl;
@BeforeEach
void setUp() {
baseUrl = "http://localhost:" + port + "/api/users";
}
/**
* 测试获取用户列表 API
*/
@Test
@DisplayName("测试获取用户列表API")
void testGetAllUsersApi() {
// Given - 准备测试数据
createTestUsers();
// When
ResponseEntity<UserDto[]> response = restTemplate.getForEntity(baseUrl, UserDto[].class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody()).hasSizeGreaterThanOrEqualTo(2);
// 验证响应数据结构
UserDto firstUser = response.getBody()[0];
assertThat(firstUser.getId()).isNotNull();
assertThat(firstUser.getName()).isNotBlank();
assertThat(firstUser.getEmail()).isNotBlank();
}
/**
* 测试根据 ID 获取用户 API
*/
@Test
@DisplayName("测试根据ID获取用户API")
void testGetUserByIdApi() {
// Given
User savedUser = createAndSaveUser("API测试用户", "api@example.com");
// When
ResponseEntity<UserDto> response = restTemplate.getForEntity(
baseUrl + "/" + savedUser.getId(),
UserDto.class
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getId()).isEqualTo(savedUser.getId());
assertThat(response.getBody().getName()).isEqualTo(savedUser.getName());
assertThat(response.getBody().getEmail()).isEqualTo(savedUser.getEmail());
}
/**
* 测试创建用户 API
*/
@Test
@DisplayName("测试创建用户API")
void testCreateUserApi() {
// Given
UserDto newUser = new UserDto();
newUser.setName("新建用户");
newUser.setEmail("newuser@example.com");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<UserDto> request = new HttpEntity<>(newUser, headers);
// When
ResponseEntity<UserDto> response = restTemplate.postForEntity(baseUrl, request, UserDto.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getId()).isNotNull();
assertThat(response.getBody().getName()).isEqualTo(newUser.getName());
assertThat(response.getBody().getEmail()).isEqualTo(newUser.getEmail());
// 验证数据库中确实创建了用户
Optional<User> savedUser = userRepository.findById(response.getBody().getId());
assertThat(savedUser).isPresent();
}
/**
* 测试更新用户 API
*/
@Test
@DisplayName("测试更新用户API")
void testUpdateUserApi() {
// Given
User existingUser = createAndSaveUser("待更新用户", "toupdate@example.com");
UserDto updateData = new UserDto();
updateData.setName("已更新用户");
updateData.setEmail("updated@example.com");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<UserDto> request = new HttpEntity<>(updateData, headers);
// When
ResponseEntity<UserDto> response = restTemplate.exchange(
baseUrl + "/" + existingUser.getId(),
HttpMethod.PUT,
request,
UserDto.class
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getId()).isEqualTo(existingUser.getId());
assertThat(response.getBody().getName()).isEqualTo(updateData.getName());
assertThat(response.getBody().getEmail()).isEqualTo(updateData.getEmail());
}
/**
* 测试删除用户 API
*/
@Test
@DisplayName("测试删除用户API")
void testDeleteUserApi() {
// Given
User userToDelete = createAndSaveUser("待删除用户", "todelete@example.com");
// When
ResponseEntity<Void> response = restTemplate.exchange(
baseUrl + "/" + userToDelete.getId(),
HttpMethod.DELETE,
null,
Void.class
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
// 验证用户已被删除
Optional<User> deletedUser = userRepository.findById(userToDelete.getId());
assertThat(deletedUser).isEmpty();
}
/**
* 测试 404 错误处理
*/
@Test
@DisplayName("测试用户不存在时的404错误")
void testUserNotFoundError() {
// Given
Long nonExistentId = 99999L;
// When
ResponseEntity<String> response = restTemplate.getForEntity(
baseUrl + "/" + nonExistentId,
String.class
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
/**
* 测试数据验证错误
*/
@Test
@DisplayName("测试数据验证错误处理")
void testValidationError() {
// Given - 无效数据(缺少必填字段)
UserDto invalidUser = new UserDto();
// 不设置 name 和 email
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<UserDto> request = new HttpEntity<>(invalidUser, headers);
// When
ResponseEntity<String> response = restTemplate.postForEntity(baseUrl, request, String.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}
// 辅助方法
private void createTestUsers() {
createAndSaveUser("测试用户1", "test1@example.com");
createAndSaveUser("测试用户2", "test2@example.com");
}
private User createAndSaveUser(String name, String email) {
User user = new User();
user.setName(name);
user.setEmail(email);
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
}
}
示例 3:性能测试
java
/**
* 性能测试
*/
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.yml")
class MapperPerformanceTest {
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
/**
* 测试大量数据映射性能
*/
@Test
@DisplayName("测试大量数据映射性能")
@Timeout(value = 5, unit = TimeUnit.SECONDS) // 5秒超时
void testLargeDataMappingPerformance() {
// Given - 创建大量测试数据
int dataSize = 10000;
List<User> users = createLargeUserList(dataSize);
// When - 执行映射并测量时间
long startTime = System.currentTimeMillis();
List<UserDto> dtos = userMapper.toDtoList(users);
long endTime = System.currentTimeMillis();
// Then
assertThat(dtos).hasSize(dataSize);
long executionTime = endTime - startTime;
System.out.println("映射 " + dataSize + " 个对象耗时: " + executionTime + " ms");
// 性能断言:平均每个对象映射时间应小于 0.1ms
double avgTimePerObject = (double) executionTime / dataSize;
assertThat(avgTimePerObject).isLessThan(0.1);
}
/**
* 测试内存使用情况
*/
@Test
@DisplayName("测试映射过程的内存使用")
void testMemoryUsage() {
// Given
Runtime runtime = Runtime.getRuntime();
runtime.gc(); // 强制垃圾回收
long memoryBefore = runtime.totalMemory() - runtime.freeMemory();
// When - 执行大量映射操作
for (int i = 0; i < 1000; i++) {
List<User> users = createLargeUserList(100);
List<UserDto> dtos = userMapper.toDtoList(users);
// 不保持引用,让 GC 可以回收
}
runtime.gc(); // 强制垃圾回收
long memoryAfter = runtime.totalMemory() - runtime.freeMemory();
// Then
long memoryUsed = memoryAfter - memoryBefore;
System.out.println("内存使用量: " + memoryUsed / 1024 / 1024 + " MB");
// 内存使用应该在合理范围内(小于 100MB)
assertThat(memoryUsed).isLessThan(100 * 1024 * 1024);
}
/**
* 测试并发映射性能
*/
@Test
@DisplayName("测试并发映射性能")
void testConcurrentMappingPerformance() throws InterruptedException {
// Given
int threadCount = 10;
int operationsPerThread = 1000;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicLong totalTime = new AtomicLong(0);
// When
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
long startTime = System.currentTimeMillis();
for (int j = 0; j < operationsPerThread; j++) {
User user = createTestUser(j);
UserDto dto = userMapper.toDto(user);
assertThat(dto).isNotNull();
}
long endTime = System.currentTimeMillis();
totalTime.addAndGet(endTime - startTime);
} finally {
latch.countDown();
}
});
}
// Then
boolean completed = latch.await(30, TimeUnit.SECONDS);
assertThat(completed).isTrue();
executor.shutdown();
long avgTime = totalTime.get() / threadCount;
System.out.println("并发映射平均耗时: " + avgTime + " ms/thread");
// 并发性能应该在合理范围内
assertThat(avgTime).isLessThan(5000); // 每个线程平均耗时小于 5 秒
}
// 辅助方法
private List<User> createLargeUserList(int size) {
List<User> users = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
users.add(createTestUser(i));
}
return users;
}
private User createTestUser(int index) {
User user = new User();
user.setId((long) index);
user.setName("用户" + index);
user.setEmail("user" + index + "@example.com");
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());
return user;
}
}
🎬 效果演示:测试执行和报告
运行测试
bash
# 运行所有测试
mvn test
# 运行特定测试类
mvn test -Dtest=UserMapperTest
# 运行特定测试方法
mvn test -Dtest=UserMapperTest#testBasicEntityToDto
# 运行集成测试
mvn test -Dtest=*IntegrationTest
# 运行性能测试
mvn test -Dtest=*PerformanceTest
# 生成测试报告
mvn surefire-report:report
# 生成测试覆盖率报告
mvn jacoco:report
测试覆盖率配置
xml
<!-- pom.xml -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>CLASS</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum> <!-- 80% 行覆盖率 -->
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum> <!-- 70% 分支覆盖率 -->
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
测试报告示例
yaml
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running io.github.nemoob.atlas.mapper.UserMapperTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.234 s - in UserMapperTest
[INFO] Running io.github.nemoob.atlas.mapper.ComplexMappingTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.456 s - in ComplexMappingTest
[INFO] Running io.github.nemoob.atlas.mapper.UserServiceIntegrationTest
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.123 s - in UserServiceIntegrationTest
[INFO] Running io.github.nemoob.atlas.mapper.MapperPerformanceTest
映射 10000 个对象耗时: 45 ms
并发映射平均耗时: 234 ms/thread
内存使用量: 12 MB
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.567 s - in MapperPerformanceTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
❓ 常见问题
Q1: 如何测试生成的 Mapper 实现类?
A: 有几种方法:
java
// 方法1:直接测试接口(推荐)
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
// 方法2:在 Spring 测试中注入
@Autowired
private UserMapper userMapper;
// 方法3:测试生成的实现类(不推荐)
private final UserMapperImpl userMapperImpl = new UserMapperImpl();
Q2: 如何 Mock 依赖的 Mapper?
A: 使用 Mockito 进行 Mock:
java
@ExtendWith(MockitoExtension.class)
class ServiceTest {
@Mock
private UserMapper userMapper;
@InjectMocks
private UserService userService;
@Test
void testServiceWithMockedMapper() {
// Given
User user = new User();
UserDto expectedDto = new UserDto();
when(userMapper.toDto(user)).thenReturn(expectedDto);
// When
UserDto result = userService.convertUser(user);
// Then
assertThat(result).isEqualTo(expectedDto);
verify(userMapper).toDto(user);
}
}
Q3: 如何测试映射的性能?
A: 使用 JMH 或简单的时间测量:
java
// 简单性能测试
@Test
@Timeout(value = 1, unit = TimeUnit.SECONDS)
void testMappingPerformance() {
List<User> users = createLargeDataSet(10000);
long startTime = System.nanoTime();
List<UserDto> dtos = userMapper.toDtoList(users);
long endTime = System.nanoTime();
long durationMs = (endTime - startTime) / 1_000_000;
System.out.println("映射耗时: " + durationMs + " ms");
assertThat(durationMs).isLessThan(500); // 应在 500ms 内完成
}
// 使用 JMH 进行更精确的性能测试
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class MapperBenchmark {
private UserMapper mapper = Mappers.getMapper(UserMapper.class);
private User user;
@Setup
public void setup() {
user = createTestUser();
}
@Benchmark
public UserDto testMapping() {
return mapper.toDto(user);
}
}
Q4: 如何测试复杂的嵌套映射?
A: 分层测试和使用测试构建器:
java
// 测试构建器模式
public class TestDataBuilder {
public static User.Builder userBuilder() {
return User.builder()
.id(1L)
.name("测试用户")
.email("test@example.com")
.createdAt(LocalDateTime.now());
}
public static Order.Builder orderBuilder() {
return Order.builder()
.id(1L)
.orderNo("TEST001")
.customer(userBuilder().build())
.items(Arrays.asList(orderItemBuilder().build()));
}
}
// 分层测试
@Test
void testNestedMapping() {
// 先测试简单映射
User user = TestDataBuilder.userBuilder().build();
UserDto userDto = userMapper.toDto(user);
assertThat(userDto).isNotNull();
// 再测试复杂嵌套映射
Order order = TestDataBuilder.orderBuilder()
.customer(user)
.build();
OrderDto orderDto = orderMapper.toDto(order);
assertThat(orderDto.getCustomer()).isEqualTo(userDto);
}
🎯 本章小结
通过本章学习,你应该掌握了:
- 单元测试:Mapper 接口和类型转换器的单元测试编写
- 集成测试:Spring Boot 环境下的集成测试设计
- 性能测试:映射性能和内存使用的测试方法
- 测试策略:测试覆盖率和质量保证的最佳实践
📖 下一步学习
在下一篇教程中,我们将学习:
- 性能优化技巧和最佳实践
- 内存管理和缓存策略
- 生产环境的监控和调优