This skill provides patterns for unit testing MapStruct mappers and custom converter classes. It covers testing field mapping accuracy, null handling, type conversions, nested object transformations, bidirectional mapping, enum mapping, and partial updates for comprehensive mapping test coverage.
When to Use
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
Instructions
Use Mappers.getMapper()
Get mapper instances for non-Spring standalone tests
Test bidirectional mapping
Verify entity→DTO and DTO→entity transformations are symmetric
Test null handling
Verify null inputs produce null outputs or appropriate defaults
Test nested objects
Verify nested objects are mapped correctly and independently
Use recursive comparison
For complex nested structures, use assertThat().usingRecursiveComparison()
Test custom mappings
Verify @Mapping annotations with custom expressions work correctly
Verify @MappingTarget updates only specified fields
Examples
Setup: Testing Mappers
Maven
<
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
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
Simple Entity to DTO Mapping
// 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
// 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
@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
// 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
// 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
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
@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
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
Constraints and Warnings
MapStruct generates code at compile time
Tests will fail if mapper doesn't generate correctly
Mapper componentModel
Spring component model requires @Component for dependency injection
Null value strategies
Configure nullValueMappingStrategy and nullValuePropertyMappingStrategy appropriately
Collection immutability
Be aware that mapping immutable collections may require special handling
Circular dependencies
MapStruct cannot handle circular dependencies between mappers
Date/Time mapping
Verify date/time objects map correctly across timezones
Expression-based mappings
Expressions in @Mapping are not validated at compile time
Troubleshooting
Null pointer exceptions during mapping
Check
nullValuePropertyMappingStrategy
and
nullValueCheckStrategy
in
@Mapper
.
Enum mapping not working
Verify
@ValueMapping
annotations correctly map source to target values.
Nested mapping produces null
Ensure nested mapper interfaces are also mapped in parent mapper.
References
MapStruct Official Documentation
MapStruct Mapping Strategies
JUnit 5 Best Practices