unit-test-mapper-converter · v1.0.1 · 2026-02-07 · sha256 f42331921469838c

unit-test-mapper-converter v1.0.1A

Immutable. This exact content is served forever at /api/v1/blob/f42331921469838c.

---
name: unit-test-mapper-converter
description: Provides patterns for unit testing mappers and converters (MapStruct, custom mappers). Validates object transformation logic in isolation. Use when ensuring correct data transformation between DTOs and domain objects.
category: testing
tags: [junit-5, mapstruct, mapper, dto, entity, converter]
version: 1.0.1
---

# Unit Testing Mappers and Converters

Test MapStruct mappers and custom converter classes. Verify field mapping accuracy, null handling, type conversions, and nested object transformations.

## 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

## Setup: Testing Mappers

### 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>
```

### Gradle
```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

### Simple Entity to DTO Mapping

```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();
  }
}
```

## Testing Custom Mapping Methods

### Mapper with @Mapping Annotations

```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

```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

```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

```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

```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

## 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

## 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](https://mapstruct.org/)
- [MapStruct Mapping Strategies](https://mapstruct.org/documentation/stable/reference/html/)
- [JUnit 5 Best Practices](https://junit.org/junit5/docs/current/user-guide/)