Claude Code Plugins

Community-maintained marketplace

Feedback

unit-test-mapper-converter

@giuseppe-trisciuoglio/developer-kit
8
0

Unit tests for mappers and converters (MapStruct, custom mappers). Test object transformation logic in isolation. Use when ensuring correct data transformation between DTOs and domain objects.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name unit-test-mapper-converter
description Unit tests for mappers and converters (MapStruct, custom mappers). Test 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

<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

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