我非常确定,如果您曾经使用过Spring并且熟悉单元测试,那么您会遇到与您不想修改的Spring应用程序上下文中注入模拟/间谍(测试双打)有关的问题。 本文介绍了一种使用Spring组件解决此问题的方法。

项目结构

让我们从项目结构开始:
mock 结合spring 注入mock bean_mysql 像往常一样提出问题,我试图显示一个非常简单的项目结构。 如果我像我们在项目中那样扩大问题的范围,我将要展示的方法可能会显示出更多的好处:

  • 我们有数十个接口和实现自动连接到列表
  • 我们希望基于现有的Spring应用程序上下文执行一些功能测试
  • 我们想要验证对于某些输入条件,某些特定的实现将执行其方法
  • 我们想存根数据库访问。

在这个例子中,我们有一个PlayerService ,它使用PlayerWebService获取一个Player 。 我们有一个applicationContext,它仅定义用于自动装配的包:

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:component-scan base-package="com.blogspot.toomuchcoding"/>

</beans>

然后我们有一个非常简单的模型:

播放器

package com.blogspot.toomuchcoding.model;

import java.math.BigDecimal;

/**
 * User: mgrzejszczak
 * Date: 08.08.13
 * Time: 14:38
 */
public final class Player {
    private final String playerName;
    private final BigDecimal playerValue;

    public Player(final String playerName, final BigDecimal playerValue) {
        this.playerName = playerName;
        this.playerValue = playerValue;
    }

    public String getPlayerName() {
        return playerName;
    }

    public BigDecimal getPlayerValue() {
        return playerValue;
    }
}

PlayerService的实现,该实现使用PlayerWebService检索有关Player数据:

PlayerServiceImpl.java

package com.blogspot.toomuchcoding.service;

import com.blogspot.toomuchcoding.model.Player;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * User: mgrzejszczak
 * Date: 08.06.13
 * Time: 19:02
 */
@Service
public class PlayerServiceImpl implements PlayerService {
    private static final Logger LOGGER = LoggerFactory.getLogger(PlayerServiceImpl.class);

    @Autowired
    private PlayerWebService playerWebService;

    @Override
    public Player getPlayerByName(String playerName) {
        LOGGER.debug(String.format("Logging the player web service name [%s]", playerWebService.getWebServiceName()));
        return playerWebService.getPlayerByName(playerName);
    }

    public PlayerWebService getPlayerWebService() {
        return playerWebService;
    }

    public void setPlayerWebService(PlayerWebService playerWebService) {
        this.playerWebService = playerWebService;
    }
}

作为数据提供者的PlayerWebService的实现(在这种情况下,我们正在模拟等待响应的时间):

PlayerWebServiceImpl.java

package com.blogspot.toomuchcoding.service;

import com.blogspot.toomuchcoding.model.Player;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * User: mgrzejszczak
 * Date: 08.08.13
 * Time: 14:48
 */
@Service
public class PlayerWebServiceImpl implements PlayerWebService {
    private static final Logger LOGGER = LoggerFactory.getLogger(PlayerWebServiceImpl.class);
    public static final String WEB_SERVICE_NAME = "SuperPlayerWebService";
    public static final String SAMPLE_PLAYER_VALUE = "1000";

    @Override
    public String getWebServiceName() {
        return WEB_SERVICE_NAME;
    }

    @Override
    public Player getPlayerByName(String name) {
        try {
            LOGGER.debug("Simulating awaiting time for a response from a web service");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            LOGGER.error(String.format("[%s] occurred while trying to make the thread sleep", e));
        }
        return new Player(name, new BigDecimal(SAMPLE_PLAYER_VALUE));
    }
}

也许项目的结构和方法不是您见过的最出色的方法之一,但我想让问题的表达保持简单;)

问题

那么到底是什么问题呢? 让我们假设我们希望自动连接的PlayerWebServiceImpl是可以验证的间谍。 而且,您不想实际更改applicationContext.xml任何内容,而是想要使用Spring上下文的当前版本。

使用模拟程序更容易,因为您可以在XML文件中定义(使用Mockito工厂方法),将bean作为模拟程序来覆盖原始实现,如下所示:

<bean id="playerWebServiceImpl" class="org.mockito.Mockito" factory-method="mock">
        <constructor-arg value="com.blogspot.toomuchcoding.service.PlayerWebServiceImpl"/>
    </bean>

那间谍呢? 因为要创建间谍,您需要给定类型的现有对象,因此问题更加严重。 在我们的示例中,我们进行了一些自动装配,因此我们必须首先创建一个PlayerWebService类型的spring bean(Spring必须连接其所有依赖项),然后将其包装在Mockito.spy(...) ,然后是否必须将其连接到其他地方…变得非常复杂,不是吗?

解决方案

您可以看到问题并不是那么容易解决的。 解决该问题的一种简单方法是使用本机Spring机制– BeanPostProcessors。 您可以查看有关如何为指定类型创建Spring BeanPostProcessor的文章-在本示例中将使用它。

让我们从检查测试类开始:

PlayerServiceImplTest.java

package com.blogspot.toomuchcoding.service;

import com.blogspot.toomuchcoding.model.Player;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.math.BigDecimal;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.doReturn;
import static org.mockito.Mockito.verify;

/**
 * User: mgrzejszczak
 * Date: 08.06.13
 * Time: 19:26
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:testApplicationContext.xml")
public class PlayerServiceImplTest {

    public static final String PLAYER_NAME = "Lewandowski";
    public static final BigDecimal PLAYER_VALUE = new BigDecimal("35000000");

    @Autowired
    PlayerWebService playerWebServiceSpy;

    @Autowired
    PlayerService objectUnderTest;

    @Test
    public void shouldReturnAPlayerFromPlayerWebService(){
        //given
        Player referencePlayer = new Player(PLAYER_NAME, PLAYER_VALUE);
        doReturn(referencePlayer).when(playerWebServiceSpy).getPlayerByName(PLAYER_NAME);

        //when
        Player player = objectUnderTest.getPlayerByName(PLAYER_NAME);

        //then
        assertThat(player, is(referencePlayer));
        verify(playerWebServiceSpy).getWebServiceName();
        assertThat(playerWebServiceSpy.getWebServiceName(), is(PlayerWebServiceImpl.WEB_SERVICE_NAME));
    }


}

在此测试中,我们希望模拟从PlayerWebService检索Player (假设正常情况下它将尝试向外界发送请求,并且我们不希望这种情况发生),并测试PlayerService返回了我们在方法存根中提供的Player ,以及我们想对Spy进行验证,以确认方法getWebServiceName()已执行并且其返回值定义得非常精确。 换句话说,我们想对方法getPlayerByName(...)进行存根,并希望通过检查getWebServiceName()方法来对间谍进行验证。

让我们检查一下测试上下文:

testApplicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <import resource="applicationContext.xml"/>
    <bean class="com.blogspot.postprocessor.PlayerWebServicePostProcessor" />
</beans>

测试上下文非常小,因为它会导入当前的applicationContext.xml并创建一个Bean,这是此示例中的关键功能– BeanPostProcessor

PlayerWebServicePostProcessor.java

package com.blogspot.postprocessor;


import com.blogspot.toomuchcoding.processor.AbstractBeanPostProcessor;
import com.blogspot.toomuchcoding.service.PlayerWebService;

import static org.mockito.Mockito.spy;

/**
 * User: mgrzejszczak
 * Date: 07.05.13
 * Time: 11:30
 */
public class PlayerWebServicePostProcessor extends AbstractBeanPostProcessor<PlayerWebService> {
    public PlayerWebServicePostProcessor() {
        super(PlayerWebService.class);
    }

    @Override
    public PlayerWebService doBefore(PlayerWebService bean) {
        return spy(bean);
    }

    @Override
    public PlayerWebService doAfter(PlayerWebService bean) {
        return bean;
    }
}

该类扩展了实现BeanPostProcessor接口的AbstractBeanPostProcessor 。 这个类背后的逻辑是注册类为其中一个想要之前任一初始化(执行某些动作postProcessBeforeInitialization )或豆(初始化之后postProcessAfterInitialization )。 我的帖子中很好地解释了AbstractBeanPostProcessor
Spring BeanPostProcessor用于指定的类型,但是有一点点变化–在我的旧文章中,抽象允许我们对Bean执行一些操作,而不能在Bean上返回包装器或代理。

如您在初始化之前使用Mockito.spy(...) PlayerWebServicePostProcessor ,我们正在使用Mockito.spy(...)方法创建一个Spy。 通过这种方式,我们在给定类型的Bean的初始化上创建了一个工厂钩子-就这么简单。 对于实现PlayerWebService接口的所有类,将执行此方法。

其他可能性

在检查该问题的当前解决方案时,我遇到了Jakub Janczak的Springockito库

我还没有使用过它,所以我不知道与此库相关的生产问题(如果有的话;)),但看起来真的很直观,很好– Jakub! 尽管如此,您仍然依赖于外部库,而在此示例中,我展示了如何使用Spring处理问题。

摘要

在这篇文章中,我展示了如何

  • 使用XML Spring配置为现有bean创建模拟
  • 创建一个BeanPostProcessor实现,该实现对给定类的bean执行逻辑
  • 对于给定的bean类,返回Spy(您也可以返回Mock)

现在,让我们看一下我的方法的优点和缺点:

优点

  • 您使用Spring本机机制为您的bean创建测试双打
  • 您不需要添加任何其他外部依赖项
  • 如果您使用AbstractBeanPostProcessor ,则只需执行很少的更改

缺点

  • 您必须熟悉内部Spring体系结构(它使用BeanPostProcessors)–但这是不利吗? ;)–实际上,如果您使用AbstractBeanPostProcessor ,则不必熟悉它–您只需提供类类型和初始化前后要发生的操作即可。
  • 它不像Springockito库中的注释那样直观

资料来源

源代码可从TooMuchCoding BitBucket存储库TooMuchCoding Github存储库中获得



参考:博客上 使用我们的JCG合作伙伴 Marcin Grzejszczak的Mockito和BeanPostProcessors在Spring注入测试双打, 用于编码成瘾者博客。



翻译自: https://www.javacodegeeks.com/2013/08/injecting-test-doubles-in-spring-using-mockito-and-beanpostprocessors.html