Reputation: 6281
I need to access a Spring bean from a custom mapping method. But I also need to be able to inject a mock of that Spring bean when unit-testing that mapping method.
Here is a minimal example of my mapper class:
@Mapper(componentModel = "spring")
public abstract class MyMapper {
private final MyBean myBean;
public MyMapper(MyBean myBean) {
this.myBean = myBean;
}
@BeforeMapping
protected MyElement initElement(MyElementDto dto) {
// custom logic using the injected MyBean to initialize MyElement
}
public abstract MyElement map(MyElementDto dto);
}
I would expect MapStruct to generate an implementation where it uses MyMapper
's parameterized constructor like so:
@Component
public class MyMapperImpl extends MyMapper {
@Autowired
public MyMapperImpl(MyBean myBean) {
super(myBean);
}
// ... mapping function implementation ...
}
However, MapStruct seems to ignore parameterized constructors and only support default parameter-less constructors.
So the question is: How can I implement this type of logic in the cleanest way so that the generated mapper implementation is unit-testable and it is possible to mock the MyBean
dependency properly?
Using MapStruct 1.3.0.Final, Spring 4.3.25.Release, Mockito 1.9.5 and Junit 4.12.
Upvotes: 1
Views: 421
Reputation: 6281
Found a solution for this case, by using @ObjectFactory
instead of @BeforeMapping
for element initialization. This also results in better code structure, separation of concerns and testability.
The element initialization logic would be defined in a separate Spring bean:
@Component
public class MyFactory {
private final MyBean myBean;
@Autowired
public MyFactory(MyBean myBean) {
this.myBean = myBean;
}
@ObjectFactory
public MyElement initialize(MyElementDto dto) {
// custom logic using the injected MyBean to initialize MyElement
}
}
The above component can be tested separately, mocking and injecting MyBean
.
Then, MyMapper
code becomes quite short, with no custom logic inside. Even interface
can be used instead of abstract class
(although both would work equally well):
@Mapper(componentModel = "spring",
uses = MyFactory.class,
injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface MyMapper {
MyElement map(MyElementDto dto);
}
The generated implementation looks like the following. First, the factory method is called to initialize the target object, then the field mappings take place on the target object returned by the factory method:
/** THIS IS AUTOMATICALLY GENERATED CODE **/
@Component
public class MyMapperImpl implements MyMapper {
private final MyFactory myFactory;
@Autowired
public MyMapperImpl(MyFactory myFactory) {
this.myFactory = myFactory;
}
@Override
public MyElement map(MyElementDto dto) {
if ( dto == null ) {
return null;
}
MyElement myElement = myFactory.initialize( dto ); // <-- FACTORY USED HERE
// ... field mapping code here, after initialization ...
return myElement;
}
}
The mapper is easily unit-testable, by mocking and injecting MyFactory
. I wanted to avoid loading any Spring context, so I initialized the MyFactoryImpl
instance manually.
@RunWith(MockitoJUnitRunner.class)
public class MyMapperTest {
@Mock
private MyFactory myFactory;
private MyMapper myMapper;
@Before
public void setUp() {
// ... myFactory stubs ...
myMapper = new MyMapperImpl(myFactory);
}
// ... tests ...
}
Upvotes: 2