我需要从自定义映射方法访问Spring bean。但是在单元测试该映射方法时,我还需要能够注入该Spring bean的模拟。
这是我的映射器类的一个最小示例:
@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);
}
我希望MapStruct生成一个使用MyMapper
的参数化构造函数的实现,如下所示:
@Component
public class MyMapperImpl extends MyMapper {
@Autowired
public MyMapperImpl(MyBean myBean) {
super(myBean);
}
// ... mapping function implementation ...
}
但是,MapStruct似乎忽略了参数化的构造函数,仅支持默认的无参数构造函数。
所以问题是:如何才能以最干净的方式实现这种逻辑,以便生成的mapper实现可以进行单元测试,并且可以正确地模拟MyBean
依赖关系?
使用MapStruct 1.3.0.Final,Spring 4.3.25.Release,Mockito 1.9.5和Junit 4.12。
答案 0 :(得分:1)
通过使用@ObjectFactory
而不是@BeforeMapping
进行元素初始化,找到了针对这种情况的解决方案。这样还可以改善代码结构,关注点分离和可测试性。
元素初始化逻辑将在单独的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
}
}
上述组件可以单独测试,可以模拟和注入MyBean
。
然后,MyMapper
代码变得很短,里面没有自定义逻辑。甚至可以使用interface
代替abstract class
(尽管两者都可以很好地工作):
@Mapper(componentModel = "spring",
uses = MyFactory.class,
injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface MyMapper {
MyElement map(MyElementDto dto);
}
生成的实现如下所示。首先,调用factory方法初始化目标对象,然后在工厂方法返回的目标对象上进行字段映射:
/** 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;
}
}
通过模拟和注入MyFactory
可以轻松地对映射器进行单元测试。我想避免加载任何Spring上下文,因此我手动初始化了MyFactoryImpl
实例。
@RunWith(MockitoJUnitRunner.class)
public class MyMapperTest {
@Mock
private MyFactory myFactory;
private MyMapper myMapper;
@Before
public void setUp() {
// ... myFactory stubs ...
myMapper = new MyMapperImpl(myFactory);
}
// ... tests ...
}