前言

前面的两篇文章讨论过《 为什么要写单元测试,何时写,写多细

》和《单元测试规范》,这篇文章介绍如何使用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;
/**
*
     * 被测对象,用@InjectMocks标注,那些被@mock标注的对象就会自动注入其中。
* 另一个注意点是这里的MatchingServiceImpl是直接new出来(Mockito 1.9版本后不new也可以),而不是通过spring容器注入的。因为这里我不需要从spring容器中
* 获得其他依赖,不需要database ,redis ,zookeeper,mq,啥都不依赖,所以直接new
*

*/

@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)}
*
*
     * if (matchingResult.isSuccess()) {
*
* doMatchingSuccess(buyOrder, topSellOrder, matchingResult, MatchingType.BUY);
*
* if (buyOrder.getCount() <= 0) {
* break;
* }
* }
*

*

*/

@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依赖

    junit
junit
4.11
test
org.mockito
mockito-all
1.10.19
test org.springframework
spring-test
4.2.5.RELEASE
test

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

阅读原文...

声明:责编内容由 CSDN 发布,CoLaBug.com 无法对本文内容的真实性提供任何保证,请自行验证并承担相关的风险与后果!CoLaBug.com 遵循[CC BY-SA 4.0]分享,并保持客观立场。如您有版权、意见、投诉等问题,请通过[eMail]联系我们处理。

原文地址:https://www.colabug.com/2340314.html

最新文章

  1. 关于在程序中 文件新生成 在用os.system()程序对新生成的文件处理 举个栗子 如下:
  2. loadrunner中web_reg_find使用使用方法
  3. 使用github和hexo搭建博客
  4. Moccakids-Tangram Puzzle 限免啦!
  5. 【php】目录、路径和文件 操作
  6. express+nodecoffee写passport登录验证实例(一)
  7. ArcGIS Server建立缓存(切图)原理解析[图解] (转载)
  8. ios开发——实用技术篇&amp;数据保存于恢复
  9. git 秘钥的生成
  10. javascript 写农场迭代
  11. C# 判断某程序是否运行
  12. 平衡树(AVL)详解
  13. TCP回射服务器程序:str_echo函数
  14. Mac下CUDA开启及Tensorflow-gpu安装
  15. spring的@Transactional(rollbackFor=Exception.class)的使用
  16. 002. https通信(CA证书认证 + 密钥商定 )
  17. HTML&amp;javaSkcript&amp;CSS&amp;jQuery&amp;ajax(11)
  18. 流水的新技术,铁打的Linux
  19. [ JAVA编程 ] double类型计算精度丢失问题及解决方法
  20. 音视频处理之PS封装的介绍与使用20180928

热门文章

  1. Promise https://www.liaoxuefeng.com/wiki/1022910821149312/1023024413276544
  2. SDUT-3364_欧拉回路
  3. datepicker插件的使用
  4. MUI - myStorage在ios safari无痕浏览模式下的解决方案
  5. 写一个nginx监控日志
  6. mysql设置text字段为not null,并且没有默认值,插入报错:doesn&#39;t have a default value
  7. iOS 避免循环导入及自定义控件delegate和protocol的相互引用的问题
  8. Python 3.x版本中的字符串
  9. 【uml】之用例图中的关系 标签: uml图形 2014-11-23 11:10 1422人阅读 评论(29)
  10. SDUT-3362_村村通公路