使用和测试生成的映射器的正确方法
Proper way of using and testing generated mapper
最近我们在开发系统的过程中遇到了一些冲突。我们发现,我们的团队有 3 种不同的测试方法,我们需要决定哪一种是最好的,并检查是否没有比这更好的方法。
首先,让我们面对一些事实:
- 我们在系统中有 3 个数据层(DTO、域对象、表)
- 我们正在使用由 mapstruct 生成的映射器将每一层的对象映射到另一层
- 我们正在使用 mockito
- 我们正在对每一层进行单元测试
现在是冲突:假设我们要测试 ExampleService
,它使用 ExampleModelMapper
将 ExampleModel
映射到 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 原则。
我们很高兴听到每种方法的任何建议、评论、优点和缺点。我们也愿意看到任何其他解决方案。
你好。
为了测试 ExampleService
,我认为模拟 mapper 及其响应是个好主意,将行为与 Mapper 测试和 MapperImpl
测试分开。
但是,您需要对 Mapper
实例进行单元测试,我更喜欢使用模拟数据进行测试,或者您也可以使用 fixture 进行测试。
要测试Mapper中引入的业务逻辑(映射规则),可以针对MapperImpl
class.
进行测试
我在这里建议两种选择。
选项 1(服务和映射器的单独单元测试套件)
如果你想进行单元测试,那么在服务中模拟你的映射器(其他依赖项以及 OFC)并仅测试服务逻辑。为映射器编写一个单独的单元测试套件。我在这里创建了一个代码示例:https://github.com/jannis-baratheon/Whosebug--mapstruct-mapper-testing-example.
示例摘录:
服务class:
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
测试主要应用程序路径并为较小的单元编写完整的单元测试。
最近我们在开发系统的过程中遇到了一些冲突。我们发现,我们的团队有 3 种不同的测试方法,我们需要决定哪一种是最好的,并检查是否没有比这更好的方法。
首先,让我们面对一些事实:
- 我们在系统中有 3 个数据层(DTO、域对象、表)
- 我们正在使用由 mapstruct 生成的映射器将每一层的对象映射到另一层
- 我们正在使用 mockito
- 我们正在对每一层进行单元测试
现在是冲突:假设我们要测试 ExampleService
,它使用 ExampleModelMapper
将 ExampleModel
映射到 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 原则。
我们很高兴听到每种方法的任何建议、评论、优点和缺点。我们也愿意看到任何其他解决方案。
你好。
为了测试 ExampleService
,我认为模拟 mapper 及其响应是个好主意,将行为与 Mapper 测试和 MapperImpl
测试分开。
但是,您需要对 Mapper
实例进行单元测试,我更喜欢使用模拟数据进行测试,或者您也可以使用 fixture 进行测试。
要测试Mapper中引入的业务逻辑(映射规则),可以针对MapperImpl
class.
我在这里建议两种选择。
选项 1(服务和映射器的单独单元测试套件)
如果你想进行单元测试,那么在服务中模拟你的映射器(其他依赖项以及 OFC)并仅测试服务逻辑。为映射器编写一个单独的单元测试套件。我在这里创建了一个代码示例:https://github.com/jannis-baratheon/Whosebug--mapstruct-mapper-testing-example.
示例摘录:
服务class:
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
测试主要应用程序路径并为较小的单元编写完整的单元测试。