Search code examples
javaunit-testingfixturesmapstruct

Simplifying and improving the unit tests of data type mappers


I have written type mappers using Mapstruct to fulfill some common tasks when mapping between entities and DTOs such as renaming properties, "cherry-picking" them by moving them out of a nested structure up to the root level etc. The initial PoC implementation looks as follows:

@Mapper(
    componentModel = "spring",
    injectionStrategy = InjectionStrategy.CONSTRUCTOR,
    uses = {DocumentationMapper.class},
    imports = {Kind.class})
public interface ConnectorMapper {
  @Mapping(source = "metadata.extendedProperties", target = "metadata.additionalProperties")
  @Mapping(source = "metadata.documentation", target = "documentation")
  @Mapping(source = "metadata.name", target = "name")
  @Mapping(source = "spec", target = "connectorSpecYaml")
  ConnectorDto fromEntity(final ConnectorYaml connectorYaml);

  @InheritInverseConfiguration
  @Mapping(target = "kind", expression = "java(Kind.Connector)")
  ConnectorYaml fromDto(final ConnectorDto connectorDto);
}

This mapper basically does the following:

  1. Rename the nested property metadata.extendedProperties to metadata.additionalProperties and vice-versa.
  2. Cherry-pick the nested metadata.documentation property for the DTO and apply mapping rules defined in DocumentationMapper (via the uses clause) and vice-versa.
  3. Cherry-pick the nested metadata.name property for the DTO and vice-versa.
  4. Rename the spec property to connectorSpecYaml and vice-versa.

In order to unit test this behavior I had to write some crazy assertions that are barely readable anymore:

@Test
public void connectorMapperFromEntity() {
  // GIVEN a connector entity with fixed values
  final var connector = FixtureBuilder.createConnectorYaml("connector");

  // WHEN mapping the entity to a DTO
  final var connectorDto = connectorMapper.fromEntity(connector);

  // THEN the mapping yields a result
  assertThat(connectorDto).isNotNull();
  // AND the name has been cherry-picked from metadata into the target
  assertThat(connectorDto.getName()).isEqualTo(connector.getMetadata().getName());
  // AND the metadata has been mapped correctly
  assertThat(connectorDto.getMetadata())
      .isEqualToIgnoringGivenFields(connector.getMetadata(), "additionalProperties");
  // AND the extended properties in metadata have been renamed correctly
  assertThat(connectorDto.getMetadata().getAdditionalProperties())
      .isEqualTo(connector.getMetadata().getExtendedProperties());
  // AND the documentation has been cherry-picked into the target
  assertThat(connectorDto.getDocumentation())
      .isEqualToIgnoringGivenFields(
          connector.getMetadata().getDocumentation(), "additionalProperties");
  // AND the extended properties in documentation have been renamed correctly
  assertThat(connectorDto.getDocumentation().getAdditionalProperties())
      .isEqualTo(connector.getMetadata().getDocumentation().getExtendedProperties());
  // AND the spec has been mapped correctly
  assertThat(connectorDto.getConnectorSpecYaml()).isEqualTo(connector.getSpec());
}

The other direction is equally rough to read due to manually building the DTO from the fixture data:

@Test
public void connectorMapperFromDto() {
  final var fixture = FixtureBuilder.createConnectorYaml("connector");
  final var connectorDto =
      ConnectorDto.builder()
          .connectorSpecYaml(fixture.getSpec())
          .metadata(
              MetadataDto.builder()
                  .additionalProperties(fixture.getMetadata().getExtendedProperties())
                  .labels(fixture.getMetadata().getLabels())
                  .build())
          .documentation(
              DocumentationDto.builder()
                  .additionalProperties(
                      fixture.getMetadata().getDocumentation().getExtendedProperties())
                  .exampleUsage(fixture.getMetadata().getDocumentation().getExampleUsage())
                  .longDescription(fixture.getMetadata().getDocumentation().getLongDescription())
                  .shortDescription(
                      fixture.getMetadata().getDocumentation().getShortDescription())
                  .exampleResponse(fixture.getMetadata().getDocumentation().getExampleResponse())
                  .build())
          .name(fixture.getMetadata().getName())
          .build();
  final var connector = connectorMapper.fromDto(connectorDto);
  assertThat(connector).isNotNull();
  assertThat(connector).isEqualTo(fixture);
}

At this point I was curious whether there are other approaches to simplify testing the mappers that arent performing simple renaming operations. I was thinking of some way to hardcode the input and expected output objects in JSON rather than building and comparing them manually. Is this a feasible approach or is there something that might be more suitable?


Solution

  • I ended up storing JSON files containing snapshots in the resources folder that I load during test execution and deserialize using Jackson.