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:
metadata.extendedProperties
to metadata.additionalProperties
and vice-versa.metadata.documentation
property for the DTO and apply mapping rules defined in DocumentationMapper
(via the uses
clause) and vice-versa.metadata.name
property for the DTO and vice-versa.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?
I ended up storing JSON files containing snapshots in the resources folder that I load during test execution and deserialize using Jackson.