简化和改进数据类型映射器的单元测试

Simplifying and improving the unit tests of data type mappers

我已经使用 Mapstruct 编写了类型映射器来完成实体和 DTO 之间映射时的一些常见任务,例如重命名属性,"cherry-picking" 通过将它们移出嵌套结构直到根级别等。初始 PoC 实现如下所示:

@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);
}

这个映射器基本上做了以下事情:

  1. 将嵌套的 属性 metadata.extendedProperties 重命名为 metadata.additionalProperties,反之亦然。
  2. 为 DTO 选择嵌套的 metadata.documentation 属性 并应用 DocumentationMapper 中定义的映射规则(通过 uses 子句),反之亦然。
  3. 为 DTO 选择嵌套的 metadata.name 属性,反之亦然。
  4. spec 属性 重命名为 connectorSpecYaml,反之亦然。

为了对这种行为进行单元测试,我不得不写一些疯狂的断言,这些断言几乎不再可读了:

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

由于从夹具数据手动构建 DTO,另一个方向同样难以阅读:

@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);
}

此时我很好奇是否有其他方法可以简化测试不执行简单重命名操作的映射器。我在想一些方法来硬编码 JSON 中的输入和预期输出对象,而不是手动构建和比较它们。这是一种可行的方法还是有更合适的方法?

您可以用随机测试数据填充 DTO。检查 here 如何执行此操作。

然后你可以写一个这样的测试用例(它有点伪代码,但希望它传达我的想法):


@Test
public void testNonNullCases() {

   ConnectorDto controlDto = generateTestData( ConnectorDto.class );

   // map hence-and-forth
   ConnectorYaml resultEntity = connectorMapper.fromDto( controlDto );
   ConnectorDto resultDto = connectorMapper.fromEntity( resultEntity );

   // use assertj to verify them (nested and all)

   assertThat( resultDto )
      .usingRecursiveComparison()
      .ignoringFields( /* fields that you want to do by hand to cover exceptions */
      .isEqualTo( controlDto );

   // do remaining assertions.

}

@Test
public void testNullCases() {

    // I would add a testcase were you leave out attributes (null) and some nested classes in your graph.. just to see whether null cases are handled correctly. You might use jacoco (or other coverage) on the generated code to drive this test

}

public static T generateTestData(Class<T> clz) {
 // get inspiration from https://github.com/Blastman/DtoTester/pull/1
}

}

我最终将 JSON 包含快照的文件存储在我在测试执行期间加载并使用 Jackson 反序列化的资源文件夹中。