欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-05 08:43:16
前言 这篇文章介绍如何使用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() <= 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

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