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
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.
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
In case we are using componentModel = "spring":
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;
}
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.
@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() {
}
}