Recently we encountered some conflict during the development of our system. We discovered, that we have 3 different approaches to testing in our team, and we need to decide which one is best and check if there is nothing better than this.
First, let's face some facts:
- we have 3 data layers in the system (DTOs, domain objects, tables)
- we are using mappers generated with mapstruct to map objects of each layer to another
- we are using mockito
- we are unit-testing each of our layers
Now the conflict: Let's assume that we want to test ExampleService
which is using ExampleModelMapper
to map ExampleModel
to ExampleModelDto
and doing some additional business logic which needs testing. We can verify the correctness of returned data in three different ways:
a) We can manually compare each field of a returned object to an expected result:
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) We can use real mapper (same as in normal logic) to compare two objects:
assertThat(returnedDto).isEqualToComparingFieldByFieldRecursivly(mapper.mapToDto(expectedEntity)))
c) And finally, we can mock mapper and its response:
final Entity entity = randomEntity();
final Dto dto = new Dto(entity.getId(), entity.getName(), entity.getOtherField());
when(mapper.mapToDto(entity)).thenReturn(dto);
We want to make tests as good as possible while keeping them elastic and change-resistant. We also want to keep to DRY principle.
We are happy to hear any pieces of advice, comments, pros, and cons of each method. We are also open to see any other solutions.
Greetings.
There are two options I'd advise here.
If you want to unit test then mock your mapper in the service (other dependencies as well OFC) and test service logic only. For the mapper write a separate unit test suite. I created a code example here: https://github.com/jannis-baratheon/stackoverflow--mapstruct-mapper-testing-example.
Excerpts from the example:
Service 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);
}
}
Mapper:
import org.mapstruct.Mapper;
@Mapper
public interface EntityMapper {
ADto toDto(AnEntity entity);
}
Service unit test:
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);
}
}
Mapper unit test:
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);
}
}
The second option is to make an integration test where you inject a real mapper to the service. I'd strongly advise not to put too much effort into validating the mapping logic in integration tests though. It's very likely to get messy. Just smoke test the mappings and write unit tests for the mapper separately.
To sum up:
I usually choose option number two where I test main application paths with MockMvc
and write complete unit tests for smaller units.