【问题标题】:Proper way of using and testing generated mapper使用和测试生成的映射器的正确方法
【发布时间】:2018-09-10 10:33:50
【问题描述】:

最近我们在系统开发过程中遇到了一些冲突。我们发现,我们的团队有 3 种不同的测试方法,我们需要确定哪种方法最好,并检查是否有比这更好的方法。

首先,让我们面对一些事实:
- 我们在系统中有 3 个数据层(DTO、域对象、表)
- 我们使用由 mapstruct 生成的映射器将每一层的对象映射到另一个
- 我们正在使用 mockito
- 我们正在对每一层进行单元测试

现在的冲突:假设我们要测试ExampleService,它使用ExampleModelMapperExampleModel 映射到ExampleModelDto,并执行一些需要测试的额外业务逻辑。我们可以通过三种不同的方式验证返回数据的正确性:

a) 我们可以手动将返回对象的每个字段与预期结果进行比较:

assertThat(returnedDto)
                .isNotNull()
                .hasFieldOrPropertyWithValue("id", expectedEntity.getId())
                .hasFieldOrPropertyWithValue("address", expectedEntity.getAddress())
                .hasFieldOrPropertyWithValue("orderId", expectedEntity.getOrderId())
                .hasFieldOrPropertyWithValue("creationTimestamp", expectedEntity.getCreationTimestamp())
                .hasFieldOrPropertyWithValue("price", expectedEntity.getPrice())
                .hasFieldOrPropertyWithValue("successCallbackUrl", expectedEntity.getSuccessCallbackUrl())
                .hasFieldOrPropertyWithValue("failureCallbackUrl", expectedEntity.getFailureCallbackUrl())

b) 我们可以使用真正的映射器(与正常逻辑相同)来比较两个对象:

assertThat(returnedDto).isEqualToComparingFieldByFieldRecursivly(mapper.mapToDto(expectedEntity)))

c) 最后,我们可以模拟映射器及其响应:

final Entity entity = randomEntity();
final Dto dto = new Dto(entity.getId(), entity.getName(), entity.getOtherField());
when(mapper.mapToDto(entity)).thenReturn(dto);

我们希望使测试尽可能好,同时保持它们的弹性和抗变化性。我们也想保持 DRY 原则。

我们很高兴听到每种方法的任何建议、cmets、优缺点。我们也愿意看到任何其他解决方案。

您好。

【问题讨论】:

  • 也许你可以在这里找到一些灵感:youtube.com/watch?v=2vEoL3Irgiw(在 45 分钟内改进你的测试驱动开发 - Jakub Nabrdalik)也许还有一些事实需要调整,比如“我们正在对每个我们的图层”...
  • 这意味着我们正在为每个服务、每个控制器和每个存储库编写单元测试。

标签: java unit-testing mocking mapping mapstruct


【解决方案1】:

这里有两种选择。

选项 1(服务和映射器的单独单元测试套件)

如果您想进行单元测试,请在服务中模拟您的映射器(其他依赖项以及 OFC)并仅测试服务逻辑。为映射器编写一个单独的单元测试套件。我在这里创建了一个代码示例:https://github.com/jannis-baratheon/stackoverflow--mapstruct-mapper-testing-example

示例摘录:

服务类:

public class AService {
    private final ARepository repository;
    private final EntityMapper mapper;

    public AService(ARepository repository, EntityMapper mapper) {
        this.repository = repository;
        this.mapper = mapper;
    }

    public ADto getResource(int id) {
        AnEntity entity = repository.getEntity(id);
        return mapper.toDto(entity);
    }
}

映射器:

import org.mapstruct.Mapper;

@Mapper
public interface EntityMapper {
    ADto toDto(AnEntity entity);
}

服务单元测试:

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import org.junit.Before;
import org.junit.Test;

public class AServiceTest {

    private EntityMapper mapperMock;

    private ARepository repositoryMock;

    private AService sut;

    @Before
    public void setup() {
        repositoryMock = mock(ARepository.class);
        mapperMock = mock(EntityMapper.class);

        sut = new AService(repositoryMock, mapperMock);
    }

    @Test
    public void shouldReturnResource() {
        // given
        AnEntity mockEntity = mock(AnEntity.class);
        ADto mockDto = mock(ADto.class);

        when(repositoryMock.getEntity(42))
                .thenReturn(mockEntity);
        when(mapperMock.toDto(mockEntity))
                .thenReturn(mockDto);

        // when
        ADto resource = sut.getResource(42);

        // then
        assertThat(resource)
                .isSameAs(mockDto);
    }
}

映射器单元测试:

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Before;
import org.junit.Test;

public class EntityMapperTest {

    private EntityMapperImpl sut;

    @Before
    public void setup() {
        sut = new EntityMapperImpl();
    }

    @Test
    public void shouldMapEntityToDto() {
        // given
        AnEntity entity = new AnEntity();
        entity.setId(42);

        // when
        ADto aDto = sut.toDto(entity);

        // then
        assertThat(aDto)
            .hasFieldOrPropertyWithValue("id", 42);
    }
}

选项 2(服务和映射器的集成测试 + 映射器单元测试)

第二个选项是进行集成测试,向服务注入真正的映射器。不过,我强烈建议不要在集成测试中验证映射逻辑。它很可能会变得混乱。只需对映射进行烟雾测试并为映射器单独编写单元测试。

总结

总结一下:

  • 服务单元测试(使用模拟映射器)+ 映射器单元测试
  • 服务集成测试(使用真实映射器)+ 映射器单元测试

我通常选择第二个选项,我使用MockMvc 测试主应用程序路径并为较小的单元编写完整的单元测试。

【讨论】:

  • 是的,我认为这也是正确的方法。但是答案并没有涵盖问题的最关键部分。在单元测试中模拟映射器时,我需要模拟它的映射方法。当我模拟映射方法时,我需要以某种方式在 ENTITY 的基础上准备 DTO 实例。除了手动映射还有其他方法吗? (这将只是再次重写映射器......)
  • 好的,我看到你通过模拟实体和 dto 对象解决了我的问题。这不是反模式吗?
  • 不,不是。如果它困扰您,您可以使用 new AnEntity()new ADto() 代替。
【解决方案2】:

为了测试ExampleService,我认为模拟映射器及其响应是一个好主意,将行为与映射器测试和MapperImpl 测试分开。

但是,您需要对Mapper 实例进行单元测试,我更喜欢使用模拟数据进行测试,或者您也可以使用夹具进行测试。

要测试 Mapper 中引入的业务逻辑(映射规则),可以针对 MapperImpl 类进行测试。

【讨论】:

  • 是的,这是我列出的想法之一。问题是 - 如何在不从头开始再次编写映射器的情况下模拟映射器响应。
  • 你不是在你的问题(在'c)'中写了这个吗?
  • @jannis 我的问题包含此解决方案,但似乎很糟糕。要使用它,我需要在每个测试中编写映射器函数。或者将它们剪掉并制作包含这些功能的类。这个类将是映射器的兄弟。我想避免这种情况。
  • @ŁukaszG 你为什么要这样做?您只需模拟映射方法以返回给定实体的给定 dto,仅此而已。这样您就可以从测试中删除映射器单元。
  • 如何在不写映射方法的情况下为给定实体创建 DTO?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多