1

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.

2
  • This question suggests another way to inject the bean, maybe it helps stackoverflow.com/questions/38807415/… Commented Jul 31, 2020 at 8:38
  • @marc I am sure there must be a non-expression solution as well. Commented Jul 31, 2020 at 17:00

1 Answer 1

2

Solution

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); } 

Generated Code

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; } } 

Unit Tests

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 ... } 
Sign up to request clarification or add additional context in comments.

1 Comment

You can also use setter injection

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.