unit-test-mapper-converter
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUnit Testing Mappers and Converters
映射器与转换器的单元测试
Test MapStruct mappers and custom converter classes. Verify field mapping accuracy, null handling, type conversions, and nested object transformations.
测试MapStruct映射器和自定义转换器类,验证字段映射准确性、空值处理、类型转换以及嵌套对象转换。
When to Use This Skill
何时使用该技能
Use this skill when:
- Testing MapStruct mapper implementations
- Testing custom entity-to-DTO converters
- Testing nested object mapping
- Verifying null handling in mappers
- Testing type conversions and transformations
- Want comprehensive mapping test coverage before integration tests
在以下场景使用该技能:
- 测试MapStruct映射器实现
- 测试自定义实体转DTO转换器
- 测试嵌套对象映射
- 验证映射器中的空值处理
- 测试类型转换与变换逻辑
- 在集成测试前确保映射测试覆盖率达标
Setup: Testing Mappers
环境搭建:测试映射器
Maven
Maven
xml
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>xml
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>Gradle
Gradle
kotlin
dependencies {
implementation("org.mapstruct:mapstruct:1.5.5.Final")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.assertj:assertj-core")
}kotlin
dependencies {
implementation("org.mapstruct:mapstruct:1.5.5.Final")
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.assertj:assertj-core")
}Basic Pattern: Testing MapStruct Mapper
基础模式:测试MapStruct映射器
Simple Entity to DTO Mapping
简单实体转DTO映射
java
// Mapper interface
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User user);
User toEntity(UserDto dto);
List<UserDto> toDtos(List<User> users);
}
// Unit test
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class UserMapperTest {
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldMapUserToDto() {
User user = new User(1L, "Alice", "alice@example.com", 25);
UserDto dto = userMapper.toDto(user);
assertThat(dto)
.isNotNull()
.extracting("id", "name", "email", "age")
.containsExactly(1L, "Alice", "alice@example.com", 25);
}
@Test
void shouldMapDtoToEntity() {
UserDto dto = new UserDto(1L, "Alice", "alice@example.com", 25);
User user = userMapper.toEntity(dto);
assertThat(user)
.isNotNull()
.hasFieldOrPropertyWithValue("id", 1L)
.hasFieldOrPropertyWithValue("name", "Alice");
}
@Test
void shouldMapListOfUsers() {
List<User> users = List.of(
new User(1L, "Alice", "alice@example.com", 25),
new User(2L, "Bob", "bob@example.com", 30)
);
List<UserDto> dtos = userMapper.toDtos(users);
assertThat(dtos)
.hasSize(2)
.extracting(UserDto::getName)
.containsExactly("Alice", "Bob");
}
@Test
void shouldHandleNullEntity() {
UserDto dto = userMapper.toDto(null);
assertThat(dto).isNull();
}
}java
// Mapper interface
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User user);
User toEntity(UserDto dto);
List<UserDto> toDtos(List<User> users);
}
// Unit test
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class UserMapperTest {
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldMapUserToDto() {
User user = new User(1L, "Alice", "alice@example.com", 25);
UserDto dto = userMapper.toDto(user);
assertThat(dto)
.isNotNull()
.extracting("id", "name", "email", "age")
.containsExactly(1L, "Alice", "alice@example.com", 25);
}
@Test
void shouldMapDtoToEntity() {
UserDto dto = new UserDto(1L, "Alice", "alice@example.com", 25);
User user = userMapper.toEntity(dto);
assertThat(user)
.isNotNull()
.hasFieldOrPropertyWithValue("id", 1L)
.hasFieldOrPropertyWithValue("name", "Alice");
}
@Test
void shouldMapListOfUsers() {
List<User> users = List.of(
new User(1L, "Alice", "alice@example.com", 25),
new User(2L, "Bob", "bob@example.com", 30)
);
List<UserDto> dtos = userMapper.toDtos(users);
assertThat(dtos)
.hasSize(2)
.extracting(UserDto::getName)
.containsExactly("Alice", "Bob");
}
@Test
void shouldHandleNullEntity() {
UserDto dto = userMapper.toDto(null);
assertThat(dto).isNull();
}
}Testing Nested Object Mapping
测试嵌套对象映射
Map Complex Hierarchies
映射复杂层级结构
java
// Entities with nesting
class User {
private Long id;
private String name;
private Address address;
private List<Phone> phones;
}
// Mapper with nested mapping
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User user);
User toEntity(UserDto dto);
}
// Unit test for nested objects
class NestedObjectMapperTest {
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldMapNestedAddress() {
Address address = new Address("123 Main St", "New York", "NY", "10001");
User user = new User(1L, "Alice", address);
UserDto dto = userMapper.toDto(user);
assertThat(dto.getAddress())
.isNotNull()
.hasFieldOrPropertyWithValue("street", "123 Main St")
.hasFieldOrPropertyWithValue("city", "New York");
}
@Test
void shouldMapListOfNestedPhones() {
List<Phone> phones = List.of(
new Phone("123-456-7890", "MOBILE"),
new Phone("987-654-3210", "HOME")
);
User user = new User(1L, "Alice", null, phones);
UserDto dto = userMapper.toDto(user);
assertThat(dto.getPhones())
.hasSize(2)
.extracting(PhoneDto::getNumber)
.containsExactly("123-456-7890", "987-654-3210");
}
@Test
void shouldHandleNullNestedObjects() {
User user = new User(1L, "Alice", null);
UserDto dto = userMapper.toDto(user);
assertThat(dto.getAddress()).isNull();
}
}java
// Entities with nesting
class User {
private Long id;
private String name;
private Address address;
private List<Phone> phones;
}
// Mapper with nested mapping
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDto toDto(User user);
User toEntity(UserDto dto);
}
// Unit test for nested objects
class NestedObjectMapperTest {
private final UserMapper userMapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldMapNestedAddress() {
Address address = new Address("123 Main St", "New York", "NY", "10001");
User user = new User(1L, "Alice", address);
UserDto dto = userMapper.toDto(user);
assertThat(dto.getAddress())
.isNotNull()
.hasFieldOrPropertyWithValue("street", "123 Main St")
.hasFieldOrPropertyWithValue("city", "New York");
}
@Test
void shouldMapListOfNestedPhones() {
List<Phone> phones = List.of(
new Phone("123-456-7890", "MOBILE"),
new Phone("987-654-3210", "HOME")
);
User user = new User(1L, "Alice", null, phones);
UserDto dto = userMapper.toDto(user);
assertThat(dto.getPhones())
.hasSize(2)
.extracting(PhoneDto::getNumber)
.containsExactly("123-456-7890", "987-654-3210");
}
@Test
void shouldHandleNullNestedObjects() {
User user = new User(1L, "Alice", null);
UserDto dto = userMapper.toDto(user);
assertThat(dto.getAddress()).isNull();
}
}Testing Custom Mapping Methods
测试自定义映射方法
Mapper with @Mapping Annotations
带有@Mapping注解的映射器
java
@Mapper(componentModel = "spring")
public interface ProductMapper {
@Mapping(source = "name", target = "productName")
@Mapping(source = "price", target = "salePrice")
@Mapping(target = "discount", expression = "java(product.getPrice() * 0.1)")
ProductDto toDto(Product product);
@Mapping(source = "productName", target = "name")
@Mapping(source = "salePrice", target = "price")
Product toEntity(ProductDto dto);
}
class CustomMappingTest {
private final ProductMapper mapper = Mappers.getMapper(ProductMapper.class);
@Test
void shouldMapFieldsWithCustomNames() {
Product product = new Product(1L, "Laptop", 999.99);
ProductDto dto = mapper.toDto(product);
assertThat(dto)
.hasFieldOrPropertyWithValue("productName", "Laptop")
.hasFieldOrPropertyWithValue("salePrice", 999.99);
}
@Test
void shouldCalculateDiscountFromExpression() {
Product product = new Product(1L, "Laptop", 100.0);
ProductDto dto = mapper.toDto(product);
assertThat(dto.getDiscount()).isEqualTo(10.0);
}
@Test
void shouldReverseMapCustomFields() {
ProductDto dto = new ProductDto(1L, "Laptop", 999.99);
Product product = mapper.toEntity(dto);
assertThat(product)
.hasFieldOrPropertyWithValue("name", "Laptop")
.hasFieldOrPropertyWithValue("price", 999.99);
}
}java
@Mapper(componentModel = "spring")
public interface ProductMapper {
@Mapping(source = "name", target = "productName")
@Mapping(source = "price", target = "salePrice")
@Mapping(target = "discount", expression = "java(product.getPrice() * 0.1)")
ProductDto toDto(Product product);
@Mapping(source = "productName", target = "name")
@Mapping(source = "salePrice", target = "price")
Product toEntity(ProductDto dto);
}
class CustomMappingTest {
private final ProductMapper mapper = Mappers.getMapper(ProductMapper.class);
@Test
void shouldMapFieldsWithCustomNames() {
Product product = new Product(1L, "Laptop", 999.99);
ProductDto dto = mapper.toDto(product);
assertThat(dto)
.hasFieldOrPropertyWithValue("productName", "Laptop")
.hasFieldOrPropertyWithValue("salePrice", 999.99);
}
@Test
void shouldCalculateDiscountFromExpression() {
Product product = new Product(1L, "Laptop", 100.0);
ProductDto dto = mapper.toDto(product);
assertThat(dto.getDiscount()).isEqualTo(10.0);
}
@Test
void shouldReverseMapCustomFields() {
ProductDto dto = new ProductDto(1L, "Laptop", 999.99);
Product product = mapper.toEntity(dto);
assertThat(product)
.hasFieldOrPropertyWithValue("name", "Laptop")
.hasFieldOrPropertyWithValue("price", 999.99);
}
}Testing Enum Mapping
测试枚举映射
Map Enums Between Entity and DTO
在实体与DTO之间映射枚举
java
// Enum with different representation
enum UserStatus { ACTIVE, INACTIVE, SUSPENDED }
enum UserStatusDto { ENABLED, DISABLED, LOCKED }
@Mapper(componentModel = "spring")
public interface UserMapper {
@ValueMapping(source = "ACTIVE", target = "ENABLED")
@ValueMapping(source = "INACTIVE", target = "DISABLED")
@ValueMapping(source = "SUSPENDED", target = "LOCKED")
UserStatusDto toStatusDto(UserStatus status);
}
class EnumMapperTest {
private final UserMapper mapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldMapActiveToEnabled() {
UserStatusDto dto = mapper.toStatusDto(UserStatus.ACTIVE);
assertThat(dto).isEqualTo(UserStatusDto.ENABLED);
}
@Test
void shouldMapSuspendedToLocked() {
UserStatusDto dto = mapper.toStatusDto(UserStatus.SUSPENDED);
assertThat(dto).isEqualTo(UserStatusDto.LOCKED);
}
}java
// Enum with different representation
enum UserStatus { ACTIVE, INACTIVE, SUSPENDED }
enum UserStatusDto { ENABLED, DISABLED, LOCKED }
@Mapper(componentModel = "spring")
public interface UserMapper {
@ValueMapping(source = "ACTIVE", target = "ENABLED")
@ValueMapping(source = "INACTIVE", target = "DISABLED")
@ValueMapping(source = "SUSPENDED", target = "LOCKED")
UserStatusDto toStatusDto(UserStatus status);
}
class EnumMapperTest {
private final UserMapper mapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldMapActiveToEnabled() {
UserStatusDto dto = mapper.toStatusDto(UserStatus.ACTIVE);
assertThat(dto).isEqualTo(UserStatusDto.ENABLED);
}
@Test
void shouldMapSuspendedToLocked() {
UserStatusDto dto = mapper.toStatusDto(UserStatus.SUSPENDED);
assertThat(dto).isEqualTo(UserStatusDto.LOCKED);
}
}Testing Custom Type Conversions
测试自定义类型转换
Non-MapStruct Custom Converter
非MapStruct自定义转换器
java
// Custom converter class
public class DateFormatter {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static String format(LocalDate date) {
return date != null ? date.format(formatter) : null;
}
public static LocalDate parse(String dateString) {
return dateString != null ? LocalDate.parse(dateString, formatter) : null;
}
}
// Unit test
class DateFormatterTest {
@Test
void shouldFormatLocalDateToString() {
LocalDate date = LocalDate.of(2024, 1, 15);
String result = DateFormatter.format(date);
assertThat(result).isEqualTo("2024-01-15");
}
@Test
void shouldParseStringToLocalDate() {
String dateString = "2024-01-15";
LocalDate result = DateFormatter.parse(dateString);
assertThat(result).isEqualTo(LocalDate.of(2024, 1, 15));
}
@Test
void shouldHandleNullInFormat() {
String result = DateFormatter.format(null);
assertThat(result).isNull();
}
@Test
void shouldHandleInvalidDateFormat() {
assertThatThrownBy(() -> DateFormatter.parse("invalid-date"))
.isInstanceOf(DateTimeParseException.class);
}
}java
// Custom converter class
public class DateFormatter {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static String format(LocalDate date) {
return date != null ? date.format(formatter) : null;
}
public static LocalDate parse(String dateString) {
return dateString != null ? LocalDate.parse(dateString, formatter) : null;
}
}
// Unit test
class DateFormatterTest {
@Test
void shouldFormatLocalDateToString() {
LocalDate date = LocalDate.of(2024, 1, 15);
String result = DateFormatter.format(date);
assertThat(result).isEqualTo("2024-01-15");
}
@Test
void shouldParseStringToLocalDate() {
String dateString = "2024-01-15";
LocalDate result = DateFormatter.parse(dateString);
assertThat(result).isEqualTo(LocalDate.of(2024, 1, 15));
}
@Test
void shouldHandleNullInFormat() {
String result = DateFormatter.format(null);
assertThat(result).isNull();
}
@Test
void shouldHandleInvalidDateFormat() {
assertThatThrownBy(() -> DateFormatter.parse("invalid-date"))
.isInstanceOf(DateTimeParseException.class);
}
}Testing Bidirectional Mapping
测试双向映射
Entity ↔ DTO Round Trip
实体 ↔ DTO 往返转换
java
class BidirectionalMapperTest {
private final UserMapper mapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldMaintainDataInRoundTrip() {
User original = new User(1L, "Alice", "alice@example.com", 25);
UserDto dto = mapper.toDto(original);
User restored = mapper.toEntity(dto);
assertThat(restored)
.hasFieldOrPropertyWithValue("id", original.getId())
.hasFieldOrPropertyWithValue("name", original.getName())
.hasFieldOrPropertyWithValue("email", original.getEmail())
.hasFieldOrPropertyWithValue("age", original.getAge());
}
@Test
void shouldPreserveAllFieldsInBothDirections() {
Address address = new Address("123 Main", "NYC", "NY", "10001");
User user = new User(1L, "Alice", "alice@example.com", 25, address);
UserDto dto = mapper.toDto(user);
User restored = mapper.toEntity(dto);
assertThat(restored).usingRecursiveComparison().isEqualTo(user);
}
}java
class BidirectionalMapperTest {
private final UserMapper mapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldMaintainDataInRoundTrip() {
User original = new User(1L, "Alice", "alice@example.com", 25);
UserDto dto = mapper.toDto(original);
User restored = mapper.toEntity(dto);
assertThat(restored)
.hasFieldOrPropertyWithValue("id", original.getId())
.hasFieldOrPropertyWithValue("name", original.getName())
.hasFieldOrPropertyWithValue("email", original.getEmail())
.hasFieldOrPropertyWithValue("age", original.getAge());
}
@Test
void shouldPreserveAllFieldsInBothDirections() {
Address address = new Address("123 Main", "NYC", "NY", "10001");
User user = new User(1L, "Alice", "alice@example.com", 25, address);
UserDto dto = mapper.toDto(user);
User restored = mapper.toEntity(dto);
assertThat(restored).usingRecursiveComparison().isEqualTo(user);
}
}Testing Partial Mapping
测试部分映射
Update Existing Entity from DTO
从DTO更新现有实体
java
@Mapper(componentModel = "spring")
public interface UserMapper {
void updateEntity(@MappingTarget User entity, UserDto dto);
}
class PartialMapperTest {
private final UserMapper mapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldUpdateExistingEntity() {
User existing = new User(1L, "Alice", "alice@old.com", 25);
UserDto dto = new UserDto(1L, "Alice", "alice@new.com", 26);
mapper.updateEntity(existing, dto);
assertThat(existing)
.hasFieldOrPropertyWithValue("email", "alice@new.com")
.hasFieldOrPropertyWithValue("age", 26);
}
@Test
void shouldNotUpdateFieldsNotInDto() {
User existing = new User(1L, "Alice", "alice@example.com", 25);
UserDto dto = new UserDto(1L, "Bob", null, 0);
mapper.updateEntity(existing, dto);
// Assuming null-aware mapping is configured
assertThat(existing.getEmail()).isEqualTo("alice@example.com");
}
}java
@Mapper(componentModel = "spring")
public interface UserMapper {
void updateEntity(@MappingTarget User entity, UserDto dto);
}
class PartialMapperTest {
private final UserMapper mapper = Mappers.getMapper(UserMapper.class);
@Test
void shouldUpdateExistingEntity() {
User existing = new User(1L, "Alice", "alice@old.com", 25);
UserDto dto = new UserDto(1L, "Alice", "alice@new.com", 26);
mapper.updateEntity(existing, dto);
assertThat(existing)
.hasFieldOrPropertyWithValue("email", "alice@new.com")
.hasFieldOrPropertyWithValue("age", 26);
}
@Test
void shouldNotUpdateFieldsNotInDto() {
User existing = new User(1L, "Alice", "alice@example.com", 25);
UserDto dto = new UserDto(1L, "Bob", null, 0);
mapper.updateEntity(existing, dto);
// Assuming null-aware mapping is configured
assertThat(existing.getEmail()).isEqualTo("alice@example.com");
}
}Best Practices
最佳实践
- Test all mapper methods comprehensively
- Verify null handling for every nullable field
- Test nested objects independently and together
- Use recursive comparison for complex nested structures
- Test bidirectional mapping to catch asymmetries
- Keep mapper tests simple and focused on transformation correctness
- Use Mappers.getMapper() for non-Spring standalone tests
- 全面测试所有映射器方法
- 验证每个可空字段的空值处理
- 独立及联合测试嵌套对象
- 对复杂嵌套结构使用递归比较
- 测试双向映射以发现不对称问题
- 保持映射器测试简洁,聚焦转换正确性
- 非Spring独立测试使用Mappers.getMapper()
Common Pitfalls
常见陷阱
- Not testing null input cases
- Not verifying nested object mappings
- Assuming bidirectional mapping is symmetric
- Not testing edge cases (empty collections, etc.)
- Tight coupling of mapper tests to MapStruct internals
- 未测试空值输入场景
- 未验证嵌套对象映射
- 假设双向映射是对称的
- 未测试边缘情况(如空集合等)
- 映射器测试与MapStruct内部实现过度耦合
Troubleshooting
故障排除
Null pointer exceptions during mapping: Check and in .
nullValuePropertyMappingStrategynullValueCheckStrategy@MapperEnum mapping not working: Verify annotations correctly map source to target values.
@ValueMappingNested mapping produces null: Ensure nested mapper interfaces are also mapped in parent mapper.
映射期间出现空指针异常:检查@Mapper注解中的和配置。
nullValuePropertyMappingStrategynullValueCheckStrategy枚举映射不生效:验证@ValueMapping注解是否正确映射源与目标值。
嵌套映射返回空值:确保嵌套映射器接口也在父映射器中配置。