16

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.

ŁukaszG
  • 596
  • 1
  • 3
  • 16
  • Maybe you can find some inspiration here: https://www.youtube.com/watch?v=2vEoL3Irgiw (Improving your Test Driven Development in 45 minutes - Jakub Nabrdalik) Also maybe some facts need to be adjusted, like "we are unit-testing each of our layers"... – Piohen Sep 10 '18 at 10:38
  • This mean that we are writing unit-tests for each service, each controller and each repository. – ŁukaszG Sep 10 '18 at 10:41

2 Answers2

16

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.

jannis
  • 4,843
  • 1
  • 23
  • 53
  • Yes, that's proper way in my opinion also. But the answe does not cover the most critical part of question. When mocking mapper in unit test, I need to mock it's mapping method. When I'm mocking mapping method I need to somehow prepare DTO instance on base of ENTITY. Is there any way other than manual mapping? (which will be just rewriting mapper once again...) – ŁukaszG Sep 18 '18 at 06:28
  • Ok, I see that you solved my problem by mocking entity and dto object. Isn't this an antipattern? – ŁukaszG Sep 28 '18 at 20:36
  • no, it's not. if it bothers you, you can use `new AnEntity()` and `new ADto()` instead. – jannis Sep 29 '18 at 05:37
1

To test ExampleService, I think it's a good idea to mock mapper and its response, separating the behavior from Mapper test and MapperImpl test.

But, you need to unit test Mapper instance, which I prefer to test with mock data or you can also test using fixture.

To test the business logic (mapping rules) introduced in Mapper, you can tests against MapperImpl class.

Pradip Karki
  • 662
  • 1
  • 8
  • 21
  • Yep, that's one of ideas which I listed. Question is - how to mock mapper response without writing mapper once again from scratch. – ŁukaszG Sep 12 '18 at 07:29
  • Didn't you write this in your question ( in 'c)' )? – jannis Sep 13 '18 at 08:08
  • @jannis My question contains this solution, but it seems bad. To use it I would need to write mapper functions in each test. Or cut them out and make class containing these functions. And this class would be a sibling to the mapper. I want to avoid this. – ŁukaszG Sep 13 '18 at 14:45
  • @ŁukaszG Why would you? You just mock the mapping methods to return a given dto for a given entity and that's all. This way you cut out the mapper unit from your test. – jannis Sep 14 '18 at 07:30
  • How will you create DTO for given entity wihout writing mapping method? – ŁukaszG Sep 14 '18 at 07:32
  • @ŁukaszG You'll mock the mapping process: for a given entity give me some DTO - `when(mapper.mapToDto(entity)).thenReturn(dto); `. Side note: You need to call me with @jannis for me to get notified about your comment. – jannis Sep 17 '18 at 11:57
  • @jannis Thanks for note. Yes, I know how to mock it. However I need to create this return DTO instance somehow. And the way to do this correctly is to MAP IT from entity. And to map it I need to use mapper or manually write mapping. There's a circle there. – ŁukaszG Sep 17 '18 at 11:59