Proper way of using and testing generated mapper

ŁukaszG picture ŁukaszG · Sep 10, 2018 · Viewed 12.8k times · Source

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.

Answer

jannis picture jannis · Sep 17, 2018

There are two options I'd advise here.

Option 1 (separate unit tests suite for service and mapper)

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

Option 2 (integration tests for service and mapper + mapper unit tests)

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.

Summary

To sum up:

  • unit tests for the service (using a mocked mapper) + unit tests for the mapper
  • integration tests for the service (with real mapper) + unit tests for the mapper

I usually choose option number two where I test main application paths with MockMvc and write complete unit tests for smaller units.