How to write Junit test for mapstruct abstract mapper injected via Spring
Asked Answered
O

10

55

I'm using MapStruct, mapstruct-jdk8 version 1.1.0.Final and defining abstract class that I inject via Spring.

I'm looking at how to be able to test them via Junit Test ? I've basicaly a main mapper that will use 2 sub mappers

@Mapper(componentModel = "spring", uses = {SubMapper1.class, SubMapper2.class})
public abstract class MainMapper {

  @Mapping(target = "field1", qualifiedByName = {"MyMapper2Name", "toEntity"})
  public abstract MyEntity toEntity(MyDto pDto);

  public MyDto fromEntity(MyEntity pEntity) {
     // Specific code, hence why I use Abstract class instead of interface. 
  }
}

I've tried several things but can't get the mapper to be instancied correctly to test it.

@RunWith(SpringRunner.class)
public class MainMapperTest {

    private MainMapper service = Mappers.getMapper(MainMapper.class);

    @Test
    public void testToEntity() throws Exception {
.....

java.lang.RuntimeException: java.lang.ClassNotFoundException: Cannot find implementation for com.mappers.MainMapper

I've also tried via @InjectMock but no dice either.

Cannot instantiate @InjectMocks field named 'service'. You haven't provided the instance at field declaration so I tried to construct the instance. However, I failed because: the type 'MainMapper is an abstract class.

And via Spring @Autowired

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.mappers.MainMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

I'm guessing this might have to do with annotation processor, and mapper not being generated when I launch test. I found this class as example.

However the class AnnotationProcessorTestRunner doesn't seems to be available before 1.2 which has no final release yet.

So my question is how do I write Junit tests to test my mapstruct abstract class mapper that I use via Spring injection in my code.

Oralee answered 24/7, 2017 at 8:13 Comment(0)
O
42

In response to @Richard Lewan comment here is how I declared my test class for the abstract class ConfigurationMapper using 2 subMappers

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ConfigurationMapperImpl.class, SubMapper1Impl.class, SubMapper2Impl.class})
public class ConfigurationMapperTest {

You use the Impl generated classes in the SpringBootTest annotation and then inject the class you want to test:

@Autowired
private ConfigurationMapper configurationMapper;

Let me know if you need more info, but from there it's straightforward. I didn't mock the subMapper, as it was better for me to test all the mapping process at once.

Oralee answered 8/9, 2017 at 9:4 Comment(0)
N
34

Addition to @TheBakker's answer: as a lighter alternative to @SpringBootTest you can use @ContextConfiguration, if you do not require the whole SpringBoot stack. His example would look like this:

@ExtendWith(SpringExtension.class) // JUnit 5
@ContextConfiguration(classes = {
            ConfigurationMapperImpl.class,
            SubMapper1Impl.class,
            SubMapper2Impl.class
        })
public class ConfigurationMapperTest {
...

With JUnit 4 use annotation RunWith instead of ExtendWith:

@RunWith(SpringRunner.class)       // JUnit 4
...
Nudi answered 8/1, 2020 at 15:48 Comment(2)
This is the most efficient way if you are writing unit test and not integration test. Please note, if you are using junit5 you need to replace @RunWith(SpringRunner.class) with @ExtendWith(SpringExtension.class)Dormant
You can avoid extending your test class with SpringExtension and adding ContextConfiguration by using injectionStrategy = InjectionStrategy.CONSTRUCTOR on the MainMapper interface. That way you can initiate instances of all mappers in your test and use the generated all arguments constructor to initatie the MainMapper. You can than fully unit test all mappers together. I posted an answer with more information and code examples.Fence
P
27

Using Mockito:

@Spy
private EntityMapper entityMapper = Mappers.getMapper(MyMapper.class);

And remeber to injects mocks in your class under test by, for example:

@InjectMocks
private MyClassUnderTest myClassUnderTest
Perez answered 4/8, 2021 at 14:53 Comment(2)
Thank you save my day with @Spy annotation. Its good idea for junit4 test developers. Because there is no solution with junit 4 on official documentation.Svelte
This also works if you have a sub mapper. You just have to add @InjectsMocks annotation to the main mapper.Susannahsusanne
C
7

You are having multiple issues:

  1. You should use Mappers#getMapper(Class) only with the default componentModel, otherwise the mapper will not be instantiated correctly. If you are getting the RuntimeException there it means that the implementation class was not generated. Make sure that you have a correct setup
  2. You need to test against the implementation MainMapperImpl and not against the abstract class.
  3. If you want to test with the spring bean then you need to be using correct ComponentScan and make sure that the implementation and the used mappers can be autowired.

The class you linked is a wrong test class and is not related to your test case. Have a look at this integration test case for spring integration.

The AnnotationProcessorTestRunner is part of our tests and is used to test the annotation processor and has been there since the beginning. It is not part of the releases.

Collin answered 24/7, 2017 at 21:39 Comment(0)
H
6

Assumming that:

  • Your MainMapper mapper gets injected into @Component ConverterUsingMainMapper

You can use the following example:

@RunWith(SpringRunner.class)
@ContextConfiguration
public class ConsentConverterTest {

    @Autowired
    MainMapper MainMapper;

    @Autowired
    ConverterUsingMainMapper converter;

    @Configuration
    public static class Config {

        @Bean
        public ConverterUsingMainMapper converterUsingMainMapper() {
            return new ConverterUsingMainMapper();
        }

        @Bean
        public MainMapper mainMapper() {
            return Mappers.getMapper(MainMapper.class);
        }
    }


    @Test
    public void test1() {
        // ... your test.
    }

}
Hoxie answered 30/11, 2018 at 15:10 Comment(2)
Does this mean all other non-mapper dependencies which have been autowired, also need to be defined as a bean inside the Config?Scaffolding
Do we need to autowire in test classes? Can't we use @InjectMocks?Chichihaerh
T
1

You can also manually create an application context with your mapper and its collaborators:

ApplicationContext context = new AnnotationConfigApplicationContext(MainMapperImpl.class, SubMapper1Impl.class, SubMapper2Impl.class);

or

ApplicationContext context = new AnnotationConfigApplicationContext("package.name.for.your.mappers");

Then get your Mapper from context

MainMapper mainMapper = context.getBean(MainMapper.class);
Thumbnail answered 2/3, 2022 at 11:9 Comment(1)
But Impl's are in generatef folder. It's not visible in classpath. At least in eclipse.Ickes
A
1

For anybody willing to solve this problem without running the test within Spring:

You can use Mockito's @InjectMock annotation instead. Assuming your mapper is FooBarMapper, relying onto subMapper QixSubMapper and you are testing a service FooBarService which use FooBarMapper, you will write something like this:

@ExtendWith(MockitoExtension.class)
public class FooBarServiceTest {
    FooBarService fooBarService;
    
    @Mock QixSubMapper qixSubMapper;
    
    @InjectMocks FooBarMapperImpl fooBarMapper;
    
    @BeforeEach
    void setup() {
        fooBarService = new FooBarService(fooBarMapper);
        Mockito.when(qixSubMapper.convert(Mockito.anyString())).thenCallRealMethod();
    }
}

Here is how it works: you declare your Mapper with @InjectMock, and you provided a mock for the submapper dependency, so when Junit will initialize the class with Mockito, it will inject the mock of the subMapper into the main Mapper.

Then all you have to do is to configure what the mock of the subMapper should do. In my case I just told to use the real method, so basically the real mappers are called but I used the @InjectMock magic to make it seems like the @Autowired worked

Apyretic answered 26/12, 2022 at 17:39 Comment(0)
D
0

It works One hundred percent !

@SpringBootTest
 public class CustomMapperTest {

@Spy
private CustomMapper mapper = Mappers.getMapper(CustomMapper.class);

@Test
public void SCENARIO_CONVERT_ACTIVITY() {
    ActivityEntity dbObj = mapper.toDto(ActivityDTO);
    Assert.assertNotNull(dbObj);
    Assert.assertEquals("XXXX", dbobj.getId());
}

@Mapper
public abstract Class CustomMapper{
  @Mapping(source="activityDto.fathername",target="surname")
  public abstract ActivityEntity toDto(ActivityDTO activityDto);
}
Diadem answered 9/5, 2022 at 14:19 Comment(2)
This config loads repositories and tries to connect to DB, but I want to only retrive mappers.Ickes
Does the @Spy annotation have any effect, when you initialize your variable manually anyway?Turning
R
0

In order to mapstruct you will use below

Mappers.getMappers(Your Mapper Class here.class)

E.g:-

OrderMapper INSTANCE = Mappers.getMapper( OrderMapper.class );

Use the INSTANCE Variable and access your mapper class methods here.

And then run with SpringRunner.class

@RunWith(SpringRunner.class)

If you find issue with implants not found or class not found on the Test class which is being tested. Then do maven clean test like below.

mvn clean test

Then you should be fine with your testing.

Reference:-

https://mapstruct.org/development/testing-mapstruct/

Rafaellle answered 11/6, 2022 at 13:32 Comment(0)
F
0

For a complete unit test, without using spring contexts or mocks, you can do something like this:

@Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        uses = {SecondMapper.class, ThirdMapper.class},
        injectionStrategy = InjectionStrategy.CONSTRUCTOR
)
public interface MyMapper {

SecondMapper and ThirdMapper are used in MyMapper. By setting injectionStrategy = InjectionStrategy.CONSTRUCTOR the generated mapper will have a full argument constructor, something like this:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-04-04T16:28:14+0200",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 21.0.1 (Eclipse Adoptium)"
)
@Component
public class MyMapperImpl implements MyMapper {

    private final SecondMapper secondMapper;
    private final ThirdMapper thirdMapper;

    @Autowired
    public MyMapperImpl(SecondMapper secondMapper, ThirdMapper thirdMapper) {
        this.secondMapper = secondMapper;
        this.thirdMapper = thirdMapper;
    }
    // other generated code
}

Finally in your test you can define an instance of MyMapper constructed with instances of secondMapper and thirdMapper.

class MyMapperTest {
    private final SecondMapper secondMapper = new SecondMapperImpl();
    private final ThirdMapper thirdMapper = new ThirdMapperImpl();
    private final MyMapper myMapper = new MyMapperImpl(secondMapper, thirdMapper);
}

No spring contexts. No mocks. Pure unit testing.

Fence answered 4/4, 2024 at 14:47 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.