Mockable spring 依赖注入映射器定义
Mockable spring dependency injection into mapper definition
我需要从自定义映射方法访问 Spring bean。但是我还需要能够在对该映射方法进行单元测试时注入该 Spring bean 的模拟。
这是我的映射器的一个最小示例 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);
}
我希望 MapStruct 生成一个使用 MyMapper
的参数化构造函数的实现,如下所示:
@Component
public class MyMapperImpl extends MyMapper {
@Autowired
public MyMapperImpl(MyBean myBean) {
super(myBean);
}
// ... mapping function implementation ...
}
不过,MapStruct似乎忽略了参数化构造函数,只支持默认的无参数构造函数。
所以问题是:我怎样才能以最干净的方式实现这种类型的逻辑,以便生成的映射器实现是可单元测试的,并且可以模拟 MyBean
依赖项正确吗?
使用 MapStruct 1.3.0.Final、Spring 4.3.25.Release、Mockito 1.9.5 和 Junit 4.12.
解决方案
通过使用 @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);
}
生成的代码
生成的实现如下所示。首先调用工厂方法初始化目标对象,然后在工厂方法返回的目标对象上进行字段映射:
/** 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 ...
}
我需要从自定义映射方法访问 Spring bean。但是我还需要能够在对该映射方法进行单元测试时注入该 Spring bean 的模拟。
这是我的映射器的一个最小示例 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);
}
我希望 MapStruct 生成一个使用 MyMapper
的参数化构造函数的实现,如下所示:
@Component
public class MyMapperImpl extends MyMapper {
@Autowired
public MyMapperImpl(MyBean myBean) {
super(myBean);
}
// ... mapping function implementation ...
}
不过,MapStruct似乎忽略了参数化构造函数,只支持默认的无参数构造函数。
所以问题是:我怎样才能以最干净的方式实现这种类型的逻辑,以便生成的映射器实现是可单元测试的,并且可以模拟 MyBean
依赖项正确吗?
使用 MapStruct 1.3.0.Final、Spring 4.3.25.Release、Mockito 1.9.5 和 Junit 4.12.
解决方案
通过使用 @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);
}
生成的代码
生成的实现如下所示。首先调用工厂方法初始化目标对象,然后在工厂方法返回的目标对象上进行字段映射:
/** 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 ...
}