18

I using MapStruct to map my entities, and I'm mocking my objects using Mockito.

I want to test a method that contains a mapping with mapStruct. The problem is the nested mapper is always null in my unit tests (works well in the application)

this is my mapper declaration :

@Mapper(componentModel = "spring", uses = MappingUtils.class)
public interface MappingDef {
     UserDto userToUserDto(User user)
}

this is my nested mapper

@Mapper(componentModel = "spring")
public interface MappingUtils {
    //.... other mapping methods used by userToUserDto

this is the method that I want to test :

@Service
public class SomeClass{
        @Autowired
        private MappingDef mappingDef;

        public UserDto myMethodToTest(){

        // doing some business logic here returning a user
        // User user = Some Business Logic

        return mappingDef.userToUserDto(user)
}

and this is my unit test :

@RunWith(MockitoJUnitRunner.class)
public class NoteServiceTest {

    @InjectMocks
    private SomeClass someClass;
    @Spy
    MappingDef mappingDef = Mappers.getMapper(MappingDef.class);
    @Spy
    MappingUtils mappingUtils = Mappers.getMapper(MappingUtils.class);

    //initMocks is omitted for brevity

    @test
    public void someTest(){
         UserDto userDto = someClass.myMethodToTest();

         //and here some asserts
    }

mappingDef is injected correctly, but mappingUtils is always null

Disclamer : this is not a duplicate of this question. He is using @Autowire so he is loading the spring context so he is doing integration tests. I'm doing unit tests, so I dont to use @Autowired

I dont want to make mappingDef and mappingUtils @Mock so I don't need to do when(mappingDef.userToUserDto(user)).thenReturn(userDto) in each use case

ihebiheb
  • 3,673
  • 3
  • 46
  • 55
  • 1
    what version of mapstruct are you using? the latest provides constructor injection for Spring, so you could mock each embedded bean and just create the instance. – Darren Forsythe Feb 19 '19 at 20:51
  • I'm using version 1.2.0. I didn't see that version 1.3.0 became stable (just last week). I will try to upgrade. Do you have an example on how to do it ? I so in the documentation that I need to add InjectionStrategy.CONSTRUCTOR, but I'm not sure I understand very well. How can I do it with my use case ? – ihebiheb Feb 20 '19 at 02:48
  • In CDI this means that MapStruct generates a constructor that takes other mappers as an argument. I'm not too familiar with Spring.. but I guess it works the same. I personally would go for a lib that performs injection for you in the test.. I think we do something similar in MapStruct unites .. – Sjaak Feb 22 '19 at 18:21

6 Answers6

12

If you are willing to use Spring test util, it's fairly easy with org.springframework.test.util.ReflectionTestUtils.

MappingDef mappingDef = Mappers.getMapper(MappingDef.class);
MappingUtils mappingUtils = Mappers.getMapper(MappingUtils.class);

...

// Somewhere appropriate
@Before
void before() {
    ReflectionTestUtils.setField(
        mappingDef,
        "mappingUtils",
        mappingUtils
    )
}
Lee Han Kyeol
  • 2,371
  • 2
  • 29
  • 44
9

force MapStruct to generate implementations with constructor injection

@Mapper(componentModel = "spring", uses = MappingUtils.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface MappingDef {
     UserDto userToUserDto(User user)
}
@Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface MappingUtils {
    //.... other mapping methods used by userToUserDto

use constructor injection, so that you can construct the class under test with a mapper.

@Service
public class SomeClass{

        private final MappingDef mappingDef;

        @Autowired
        public SomeClass(MappingDef mappingDef) {
            this.mappingDef = mappingDef; 
        }

        public UserDto myMethodToTest(){

        // doing some business logic here returning a user
        // User user = Some Business Logic

        return mappingDef.userToUserDto(user)
}

Test SomeClass. Note: its not the mapper that you test here, so the mapper can be mocked.

@RunWith(MockitoJUnitRunner.class)
public class SomeClassTest {

    private SomeClass classUnderTest;

    @Mock
    private MappingDef mappingDef;

    @Before init() {
        classUnderTest = new SomeClass(mappingDef);
        // defaultMockBehaviour: 
when(mappingDef.userToUserDto(anyObject(User.class).thenReturn(new UserDto());
    } 

    @test
    public void someTest(){
         UserDto userDto = someClass.myMethodToTest();

         //and here some asserts
    }

And in a true unit test, test the mapper as well.

@RunWith(MockitoJUnitRunner.class)
public class MappingDefTest {

  MappingDef classUnderTest;

  @Before
  void before() {
       // use some reflection to get an implementation
      Class aClass = Class.forName( MappingDefImpl.class.getCanonicalName() );
      Constructor constructor =
        aClass.getConstructor(new Class[]{MappingUtils.class});
      classUnderTest = (MappingDef)constructor.newInstance( Mappers.getMapper( MappingUtils.class ));
  }

  @Test
  void test() {
     // test all your mappings (null's in source, etc).. 
  }


ihebiheb
  • 3,673
  • 3
  • 46
  • 55
Sjaak
  • 3,602
  • 17
  • 29
  • Thanks. But it would be nice if it was possible to Spy mappingDef instead of mockink it. It prevents me from writing all the when...thenReturn statements – ihebiheb Mar 07 '19 at 16:54
  • 1
    It is possible, but I would not advice it. Have a look at this https://stackoverflow.com/questions/12827580/mocking-vs-spying-in-mocking-frameworks.. – Sjaak Mar 07 '19 at 17:18
  • I know that it is not recommended, but I'm nearly always using the default mapping in mapStruct. So It is like I'm im testing the mapStruct library. Spying it will save me too much time. – ihebiheb Mar 07 '19 at 21:05
2

There is no need to use reflections. The simplest way for me was the following:

@RunWith(MockitoJUnitRunner.class)
public class NoteServiceTest {

@InjectMocks
private SomeClass someClass;

@Spy
@InjectMocks
MappingDef mappingDef = Mappers.getMapper(MappingDef.class);
@Spy
MappingUtils mappingUtils = Mappers.getMapper(MappingUtils.class);

this however only works on the first level of nested Mappers. If you have a Mapper that uses a Mapper which uses a thrid Mapper than you need to use ReflectionTestUtils to Inject the third Mapper into the second Mapper.

GJohannes
  • 1,408
  • 1
  • 8
  • 14
1

As a variant on Sjaak’s answer, it is now possible to rely on MapStruct itself to retrieve the implementation class while also avoiding casts by properly using generics:

Class<? extends MappingDef> mapperClass = Mappers.getMapperClass(MappingDef.class);
Constructor<? extends MappingDef> constructor = mapperClass.getConstructor(MappingUtils.class);
MappingDef mappingDef = constructor.newInstance(Mappers.getMapper(MappingUtils.class));

This could probably even be made completely generic by inspecting the constructor, finding all mappers it requires as arguments and recursively resolving those mappers.

Didier L
  • 18,905
  • 10
  • 61
  • 103
  • Nice one! When you say, it is now possible, you mean that this is a new feature added to Mapstruct ? – ihebiheb May 13 '22 at 14:47
  • `getMapperClass()` was added in MapStruct 1.3, I didn’t actually check the release dates vs. other answers’ time – Didier L May 15 '22 at 21:49
0

So, try this:

Maven:

      <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <scope>test</scope>
        </dependency>
@ComponentScan(basePackageClasses = NoteServiceTest.class)
@Configuration
public class NoteServiceTest {

    @Autowired
    private SomeClass someClass;
    private ConfigurableApplicationContext context;

    @Before
    public void springUp() {
        context = new AnnotationConfigApplicationContext( getClass() );
        context.getAutowireCapableBeanFactory().autowireBean( this );
    }

    @After
    public void springDown() {
        if ( context != null ) {
            context.close();
        }
    }

    @test
    public void someTest(){
         UserDto userDto = someClass.myMethodToTest();

         //and here some asserts
    }

Even better would be use constructor injection all the way... Also in SomeClass and by using @Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR).. Then you don't need spring / spring mocking in your test cases.

Sjaak
  • 3,602
  • 17
  • 29
  • But here you are doing integration tests, you are loading the spring contxt using @Autowire. From the [spring documentation](https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html) : The Spring Framework provides first-class support for integration testing in the spring-test module. Can you give me an example using constructor injection strategy for my use case. I'm not sure to understand it – ihebiheb Mar 04 '19 at 13:30
  • It is sometimes a bit unclear were integration tests start in this and unit test ends (matter of taste as well). But I'll add another answer – Sjaak Mar 05 '19 at 17:49
0

As mentioned you can use the injectionStrategy = InjectionStrategy.CONSTRUCTOR for mappers using other mappers (in this case for the MappingDef).

And than in the test simply:

@Spy
MappingUtils mappingUtils = Mappers.getMapper(MappingUtils.class);

@Spy
MappingDef mappingDef = new MappingDefImpl(mappingUtils);

Maybe not the most elegant but it works.

Andrzej Sawoniewicz
  • 1,541
  • 2
  • 16
  • 18