javaspring-bootspring-testmapstruct

SpringBootTest context with mapstruct.Mapper bean


I'm working in an application where I need to map some input to an entity class and I'm using mapstruct like this:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
import org.mapstruct.NullValueCheckStrategy;

import java.math.BigDecimal;

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING,
        nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
public interface MyEntityMapper {

    @Mapping(source = "source1", target = "target1")
    @Mapping(source = "source2", target = "target2")
    MyEntity toMyEntity(MyInput input);
}

And then I'm adding a SpringBoot test to check the mapping is being done as expected

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(classes = MyEntityMapper.class)
class MyEntityMapperTest {

    @Autowired
    private MyEntityMapper underTest;

    @Test
    @DisplayName("Entity mapped when input provided")
    void test_toMyEntity_returnsMappedEntity_whenInputProvided() {
        MyEntity result = underTest.toMyEntity(MyInputRepository.basicInput());
        assertThat(result).isNotNull();
        // more asserts if needed
    }
}

But when I run this test I get the following error:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.example.mapper.MyEntityMapperTest': Unsatisfied dependency expressed through field 'underTest'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.mapper.MyEntityMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at ...

Checking logs in detail and doing some web research I found it may be a context initialization issue. Seems that SpringBoot uses some listeners to prepare some beans. When Spring is starting I find the following message:

{
    "logger_name": "com.example.mapper.MyEntityMapperTest",
    "message": "Started MyEntityMapperTest in 0.849 seconds (JVM running for 2.076)",
    "log_level": "INFO",
    "log_type": "DEFAULT",
    "thread_name": "main",
    "stack_trace": "-"
}



============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

    None


Negative matches:
-----------------

    None


Exclusions:
-----------

    None


Unconditional classes:
----------------------

    None



{
    "logger_name": "org.springframework.test.context.TestContextManager",
    "message": "Caught exception while allowing TestExecutionListener [org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@1852a3ff] to prepare test instance [com.example.mapper.MyEntityMapperTest@3ce53f6a]",
    "log_level": "ERROR",
    "log_type": "DEFAULT",
    "thread_name": "main",
    "stack_trace": "o.s.b.f.NoSuchBeanDefinitionException: ..."
}

Note: formatted log messages in JSON for better readability

Workaround

I found a way to workaround this by simply loading the complete Spring context. It seems that Spring, in such circumstances, runs correctly all the bootstraping and loads my mapper bean correctly. However, this implies a lot of time to run a simple test and the need to add an innecessary test configuration, like:

@SpringBootTest
@ActiveProfiles("test")
class MyEntityMapperTest {...}

With this my test runs correctly BUT with the disadvantages described before.

The question

Is there any way to tell Spring to bootstrap such that the mapper bean is ready when instantiating the test class bean?

Thanks in advance for your answers/comments


Solution

  • In case we are using componentModel = "spring":

    1. If mapper doesn't use other mappers (or any other components), there's no point in starting a Spring context, so the test could look like this:
    import org.junit.jupiter.api.Test;
    import static org.assertj.core.api.Assertions.assertThat;
    import com.example.my.mapper.MyMapstructMapper;
    import com.example.my.mapper.MyMapstructMapperImpl;
    
    class MyMapstructMapperTest {
    
        private MyMapstructMapper underTest = new MyMapstructMapperImpl();
    
    
        @Test
        void myTest() {
            MyDto source = ... // create source
            MyEntity result = underTest.toMyEntity(source);
            assertThat(result).isNotNull();
            // more asserts if needed
        }
    }
    

    If we write a Spring Boot test here with a preconfigured context, as you wanted, it doesn't have any advantage, in fact, quite the opposite, because starting the context here is unnecessary:

    @SpringBootTest(classes = MyMapstructMapperImpl.class) // here must be Impl, not interface
    class MyMapstructMapperTest {
        @Autowired
        private MyMapstructMapper underTest;
    
    }
    
    1. Further, if our mapper uses other mappers or components, then we must consider whether it still makes sense to write a unit test and instantiate our mapper manually, for example as follows:
    private MyMapstructMapper underTest = new MyMapstructMapperImpl(new OtherMapstructMapperImpl());
    

    In this case, the MyMapstructMapper uses only one other mapper, therefore it is acceptable fmpov to instantiate them manually via constructors using new.

    1. But it can be the case that the mapper uses, many other mappers or components, such as:
    @Mapper(uses = {
            BillingAccountMapper.class, // mapstruct mapper
            CommonMapper.class,
            NoteMapper.class, // not mapstruct mapper, that uses other components inside
            OrderPriceMapper.class,
            RelatedEntityMapper.class,
            RelatedPartyMapper.class,
            RelatedPlaceMapper.class,
            WorkCharacteristicsMapper.class,
            WorkOrderItemMapper.class,
    })
    public interface WorkOrderMapper {
        WorkOrderWFCreate toWorkOrderWf(WorkOrder workOrder);
    }
    

    In this case, creating a mapper manually or via context configuration can be really complex (because the mapper uses other mappers, which use still other mappers, and so on), so I prefer to use a Spring Boot test with lazy bean initialization, which will prevent unused beans from loading during the test:

    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest(properties = "spring.main.lazy-initialization=true")
    class WorkOrderMapperTest {
    
        @Autowired
        private WorkOrderMapper workOrderMapper;
    
    
        @Test
        void test() {
             
        }
    }