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