基于Springboot+Junit+Mockito做单元测试的示例

 更新时间:2018年02月10日 14:28:19   作者:鼯鼠的技术博客  
本篇文章主要介绍了基于Springboot+Junit+Mockito做单元测试的示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

前言

这篇文章介绍如何使用Springboot+Junit+Mockito做单元测试,案例选取撮合交易的一个类来做单元测试。

单元测试前先理解需求

要写出好的单测,必须先理解了需求,只有知道做什么才能知道怎么测。但本文主要讲mockito的用法,无需关注具体需求。所以本节略去具体的需求描述。

隔离外部依赖

Case1. 被测类中被@Autowired 或 @Resource 注解标注的依赖对象,如何控制其返回值

以被测方法 MatchingServiceImpl.java的matching(MatchingOrder buyOrder, MatchingOrder sellOrder)为例

被测类MatchingServiceImpl

public class MatchingServiceImpl implements MatchingService {
  private static final Logger log = LoggerFactory.getLogger(MatchingServiceImpl.class);
  @Autowired
  private QuoteService quoteService;
  ...
  public MatchingResult matching(MatchingOrder buyOrder, MatchingOrder sellOrder) {
    int currentPrice = quoteService.getCurrentPriceByProduct(buyOrder.getProductCode());
    MatchingResult result = new MatchingResult();
    if (sellOrder != null && buyOrder != null &&
        sellOrder.getPrice() <= buyOrder.getPrice()) {
    ...
  }
}

matching方法中的quoteService.getCurrentPriceByProduct(buyOrder.getProductCode());要访问Redis获取当前报价,这里我们需要把外部依赖quoteService mock掉,控制getCurrentPriceByProduct方法的返回值。使用mockito可以做到,具体如下:

测试类MatchingServiceImplTest

public class MatchingServiceImplTest extends MockitoBasedTest {
  /**
   * 被@Mock标注的对象会自动注入到被@InjectMocks标注的对象中
   */
  @Mock
  private QuoteService quoteService;
  /**
   * <pre>
   * 被测对象,用@InjectMocks标注,那些被@mock标注的对象就会自动注入其中。
   * 另一个注意点是这里的MatchingServiceImpl是直接new出来(Mockito 1.9版本后不new也可以),而不是通过spring容器注入的。因为这里我不需要从spring容器中
   * 获得其他依赖,不需要database ,redis ,zookeeper,mq,啥都不依赖,所以直接new
   * </pre>
   */
  @InjectMocks
  private MatchingServiceImpl matchingService = new MatchingServiceImpl();
  @Test
  public void testMatching_SuccessWhenCurrentPriceBetweenBuyPriceAndSellPrice() {
    MatchingOrder buyOrder = new MatchingOrder();
    buyOrder.setPrice(1000);
    buyOrder.setCount(23);
    MatchingOrder sellOrder = new MatchingOrder();
    sellOrder.setPrice(800);
    sellOrder.setCount(20);
    // 方法打桩(Method stubbing)
    // when(x).thenReturn(y) :当指定方法被调用时返回指定值
    Mockito.when(quoteService.getCurrentPriceByProduct(Mockito.anyString())).thenReturn(900);
    MatchingResult result = matchingService.matching(buyOrder, sellOrder);
    org.junit.Assert.assertEquals(true, result.isSuccess());// 断言撮合是否成功
    org.junit.Assert.assertEquals(20, result.getTradeCount());// 断言成交数量
    org.junit.Assert.assertEquals(900, result.getTradePrice()); // 断言最新报价是否符合预期
  }

Case2. 被测函数A调用被测类其他函数B,怎么控制函数B的返回值?

比如,MatchingServiceImpl中有个函数startBuyProcess,它里面调用了该类中的其他函数,如getTopSellOrder,matching,如何控制这两个函数的返回值?
这里要解决的问题其实是怎么对一个类”部分mock”–被测类的被测方法(如startBuyProcess)要真实执行,而另一些方法(如getTopSellOrder)则是要打桩(不真正进去执行)。

被测类MatchingServiceImpl

protected void startBuyProcess(MatchingOrder buyOrder, boolean waitForMatching) {
    while (true) {
      //对手方最优价
      MatchingOrder topSellOrder = getTopSellOrder(buyOrder.getProductCode());
      MatchingResult matchingResult = matching(buyOrder,topSellOrder);
      if(matchingResult.isSuccess()) {
        doMatchingSuccess(buyOrder,topSellOrder,matchingResult,MatchingType.BUY);
        if(buyOrder.getCount() <= 0) {
          break;
        }
      }else {
        if(waitForMatching) {
          //加入待撮合队列
          addToMatchingBuy(buyOrder);
        }else {
          //撤单
          sendCancleMsg(buyOrder);
        }
        break;
      }
    }
  }

利用Mockito.spy()可以做到“部分Mock”

测试类MatchingServiceImplTest.testStartBuyProcess_InCaseOfMatchingSuccess

/**
   *
   * 测试StartBuyProcess方法在撮合成功后的处理是否符合预期,即测试startBuyProcess方法进入下面这个判断分支后的行为
   * {@link MatchingServiceImpl#startBuyProcess(MatchingOrder, boolean)}
   *
   * <pre>
   * if (matchingResult.isSuccess()) {
   *
   *   doMatchingSuccess(buyOrder, topSellOrder, matchingResult, MatchingType.BUY);
   *
   *   if (buyOrder.getCount() &lt;= 0) {
   *     break;
   *   }
   * }
   * </pre>
   *
   */
  @Test
  public void testStartBuyProcess_InCaseOfMatchingSuccess() {
    MatchingOrder buyOrder = new MatchingOrder();
    buyOrder.setPrice(700);
    buyOrder.setCount(23);
    // 用Mockito.spy()对matchingService进行部分打桩
    matchingService = Mockito.spy(matchingService);
    MatchingResult firstMatchingResult = new MatchingResult();
    firstMatchingResult.setSuccess(true);
    firstMatchingResult.setTradeCount(20);
    MatchingResult secondMatchingResult = new MatchingResult();
    secondMatchingResult.setSuccess(false);
    // doReturn(x).when(obj).method() 对方法打桩,打桩后,程序执行这些方法时将按照预期返回指定值,未被打桩的方法将真实执行
    // 两个doReturn表示第一次调用matchingService.matching时返回firstMatchingResult,第二次调用返回secondMatchingResult
    // 因为startBuyProcess里有个while循坏,可能会多次执行matching方法
    Mockito.doReturn(firstMatchingResult).doReturn(secondMatchingResult).when(matchingService)
        .matching(Mockito.any(MatchingOrder.class), Mockito.any(MatchingOrder.class));
    MatchingOrder sellOrder = new MatchingOrder();
    sellOrder.setPrice(600);
    sellOrder.setCount(20);
    // 对getTopSellOrder方法打桩
    Mockito.doReturn(sellOrder).when(matchingService).getTopSellOrder(Mockito.anyString());
    // 对外部依赖jedis的方法进行打桩
    Mockito.when(jedisClient.incrBy(Mockito.anyString(), Mockito.anyLong())).thenReturn(0L);
    // startBuyProcess是被测函数,不打桩,会真实执行
    matchingService.startBuyProcess(buyOrder, true);
    // 后面的校验和断言是测试doMatchingSuccess方法的行为的,这也是这个测试的目的
    // verify可用来校验,某个类的方法被执行过多少次,这里是校验jedisClient.zremFirst是否被执行过1次
    Mockito.verify(jedisClient, Mockito.times(1)).zremFirst(Mockito.anyString());
    org.junit.Assert.assertEquals(3, buyOrder.getCount());
    org.junit.Assert.assertEquals(0, sellOrder.getCount());
  }

spy的用法已经演示完毕,下面从testStartBuyProcess_InCaseOfMatchingSuccess说下单元测试的“粒度”。

testStartBuyProcess_InCaseOfMatchingSuccess的目的是想测doMatchingSuccess,我们费了很大劲才把前面的一堆准备工作做完,才能去测doMatchingSuccess。

更好的实践应该是另起测试方法去单独测doMatchingSuccess,关注点也集中很多,doMatchingSuccess覆盖完了,再测startBuyProcess其实就只是覆盖下它本身的判断分支就行了。覆盖率照样达到,而且测试代码也更容易维护,testStartBuyProcess_InCaseOfMatchingSuccess由于考虑的职责太多,它很容易受到变化的影响,细小的东西改变,可能就会影响它的正常工作。

引入测试框架Maven依赖

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.11</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-all</artifactId>
  <version>1.10.19</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <version>4.2.5.RELEASE</version>
  <scope>test</scope>
</dependency>

springboot+junit+mockito的上下文构建

MockitoBasedTest

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestApplication.class)
public abstract class MockitoBasedTest {
  @Before
  public void setUp() throws Exception {
    // 初始化测试用例类中由Mockito的注解标注的所有模拟对象
    MockitoAnnotations.initMocks(this);
  }
}
// 其他测试类继承MockitoBasedTest

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Java中的重要核心知识点之继承详解

    Java中的重要核心知识点之继承详解

    继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为
    2021-10-10
  • 二维码生成Java实现代码

    二维码生成Java实现代码

    这篇文章主要为大家详细介绍了二维码生成Java实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-05-05
  • Java 函数编程详细介绍

    Java 函数编程详细介绍

    这篇文章主要介绍了Java函数式编程,lambda表达式可以被认为是一个匿名函数,可以在函数接口的上下文中使用。函数接口是只指定一个抽象方法的接口,下面来看文章的详细内容,需要的朋友可以参考下
    2021-11-11
  • Spring动态添加定时任务的实现思路

    Spring动态添加定时任务的实现思路

    这篇文章主要介绍了Spring动态添加定时任务的实现思路,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-07-07
  • SpringBoot2如何集成Elasticsearch6.x(TransportClient方式)

    SpringBoot2如何集成Elasticsearch6.x(TransportClient方式)

    这篇文章主要介绍了SpringBoot2如何集成Elasticsearch6.x(TransportClient方式)问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • 利用javadoc注释自动生成Swagger注解

    利用javadoc注释自动生成Swagger注解

    由于现在controller方法上面没有swagger注解,只能拿到接口url地址,无法获得接口功能描述,所以本文为大家介绍一下如何利用javadoc注释自动生成Swagger注解,感兴趣的可以了解下
    2023-08-08
  • 解决Maven打包只有几十K,运行报错no main manifest attribute问题

    解决Maven打包只有几十K,运行报错no main manifest attribute

    这篇文章主要介绍了解决Maven打包只有几十K,运行报错no main manifest attribute问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • WIN10系统中添加bat脚本重启jar服务

    WIN10系统中添加bat脚本重启jar服务

    在bat脚本中执行java服务,命令与cmd中类似,下面这篇文章主要给大家介绍了关于WIN10系统中添加bat脚本重启jar服务的相关资料,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-12-12
  • 一篇文章教你将JAVA的RabbitMQz与SpringBoot整合

    一篇文章教你将JAVA的RabbitMQz与SpringBoot整合

    这篇文章主要介绍了如何将JAVA的RabbitMQz与SpringBoot整合,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2021-09-09
  • 定时任务注解@Scheduled不生效问题及解决

    定时任务注解@Scheduled不生效问题及解决

    这篇文章主要介绍了定时任务注解@Scheduled不生效问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06

最新评论