Mockito单元测试
Posted 杀手不太冷!
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mockito单元测试相关的知识,希望对你有一定的参考价值。
文章目录
Mockito单元测试
为什么要使用Mock?
比如你现在想要测试一个方法是否是正常的,但是这个方法中有很多调用数据库的代码,那么我们就可以在每个调用数据库的地方打桩,模拟一下访问完数据库之后的返回值,这样我们就可以在测试的时候避免访问数据库了,可以非常高效地完成我们的单元测试,已达到验证我们写的方法到底对不对的目的。
先试想一个这样的场景,如果想要模拟一个人被车撞之后,会出现什么样的运动轨迹,那我们要做实验的时候肯定是不能用一个活生生的人做实验的对不对?
我们的Mock模拟对象也是一样的,因为真实对象由于一些原因很难被拿来直接做测试,所以我们就必须要模拟一个和真实对象一样的对象,那这个模拟的对象做测试,而Mock就是用来模拟对象的。
那么对象在什么时候是不方便被直接拿来做实验的啊?你比如:
- 真实对象的行为很难触发(例如,网络错误);
- 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
导入依赖
我们的Mockito单元测试需要配合Junit单元测试一起使用,因此首先我们需要导入这两个依赖,如下图:
<dependencies>
<!--导入Mockito单元测试依赖-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
<!--导入junit单元测试依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
import导入包
使用Mock模拟测试某个类中的某个方法是否可以成功执行
如果我们想要测试某个类中的某个方法是否可以执行,我们不用直接调用这个类的这个方法,我们可以模拟一下这个类,让模拟的对象调用这个方法,然后再去检验一下这个模拟对象是否成功的调用到了这个方法,只要这个模拟对象成功调用到了这个方法,那么就说明,我们真实类中的这个方法是可以被成功执行的。这就是使用mockito来进行某个类的单元测试,如下图:
/**
* 通过mock模拟一个对象,然后检验这个对象的某个方法是否可以执行
* */
@Test
public void test1(){
//模拟创建一个List对象
List mock = mock(List.class);
//使用mock模拟出来的List对象,让这个对象添加一个元素1,(我们其实是为了验证List类中的add方法是否正常)
mock.add(1);
//验证模拟的对象List的add(1)方法是否执行,如果正常发生了,说明List类中的add(1)方法是正常的。这样我们的单元测试就算通过,
//verify()的作用主要是检验我们模拟出来的这个对象中的方法是否成功执行,如果成功执行控制台什么信息都没有,如果没有成功
//执行,控制台会报错误信息
verify(mock).add(1);
}
使用Mock模拟某个类的方法,自己给这个方法返回我们指定的值
我们在测试一个控制器中的方法的时候,这个控制器中肯定是有一些方法是需要访问数据库的,但是我们自己在进行单元测试的时候,其实不必访问数据库,我们只需要知道访问数据库之后得到的这个值是什么,所以我们就可以使用Mock来模拟出访问数据库的方法返回的值,下面的这个例子就是我们自己给某个类中的方法直接指定一个返回值,如下图:
/**
* 模拟对象中的某个方法,给这个方法指定一个返回值,那么我们再执行这个模拟对象的方法的时候,返回的值就不再是真实
* 对象返回的值,而应该是我们自己设置的返回值。
*
* 比如我们这里有一个Iterator迭代器,原本调用迭代器对象的next()方法之后返回的值是集合中的下一个元素,我们这里来模拟
* 这个方法的返回值,模拟的是第一次调用next()方法返回值是"hello",第二次调用next()方法返回值是"world",第三次以及往后调用
* next()方法返回值是"abc"
*
* 使用到了when(),thenReturn()方法,第一个thenReturn()代表第一次执行iterator.next()方法的返回值是"hello",
* 第二个thenReturn()代表第二次执行iterator.next()方法的返回值是"world",
* 第三个thenReturn()代表第三次即以后执行iterator.next()方法的返回值都是"abc"
*
* 还是用到了assertEquals(猜想值,变量)断言方法
* */
@Test
public void test2(){
//使用mock模拟出一个Iterator类
Iterator iterator = mock(Iterator.class);
//自己设置迭代器对象方法next()的返回值
when(iterator.next()).thenReturn("hello").thenReturn("world").thenReturn("abc");
//使用mock模拟的iterator对象,去看看iterator调用next()方法之后的返回值是否是我们想的那样
String result = iterator.next() + " " + iterator.next() + " " + iterator.next()+" "+iterator.next();
//使用断言验证猜想的结果是否正确
assertEquals("hello world abc abc",result);
}
使用Mock模拟某个方法调用后会抛出指定的异常
/**
* 使用Mock模拟对象,规定某个方法要抛出一个异常
* */
@Test(expected = IOException.class)
public void test3() throws IOException {
OutputStream outputStream = mock(OutputStream.class);
//我们自己规定当执行OutputStream对象的close()方法的时候,会主动的抛出一个IOException异常
doThrow(new IOException()).when(outputStream).close();
outputStream.close();
}
使用Mock模拟测试某个类中的某个方法(加上参数)调用的次数
/**
* 模拟类中的某个方法(加上对应的参数)调用的次数
*
* times(次数)表示调用几次
* never()表示从没有调用过
* atLeastOnce()表示至少调用多少1次
* atLeast(次数)表示至少调用n次
* atMost(次数)表示最多调用n次
* */
@Test
public void test5(){
List list = mock(List.class);
list.add(1);
list.add(2);
list.add(2);
list.add(3);
list.add(3);
list.add(3);
//验证是否被调用一次,等效于下面的times(1)
verify(list).add(1);
verify(list,times(1)).add(1);
//验证是否被调用2次
verify(list,times(2)).add(2);
//验证是否被调用3次
verify(list,times(3)).add(3);
//验证是否从未被调用过
verify(list,never()).add(4);
//验证至少调用一次
verify(list,atLeastOnce()).add(1);
//验证至少调用2次
verify(list,atLeast(2)).add(2);
//验证至多调用3次
verify(list,atMost(3)).add(3);
}
使用@Mock注解生成模拟对象
/**
* @Date 2021/11/10 15:47
* @Author 望轩
*
* 使用@Mock注解生成一个模拟对象,注意这样的话必须要在加上一个注解@RunWith(MockitoJUnitRunner.class),
* 要不然的话应用到的mockList或者是iterator会是null值
*/
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest2 {
@Mock
private List mockList;
@Mock
private Iterator iterator;
@Test
public void test1(){
mockList.add(1);
verify(mockList).add(1);
}
@Test
public void test2(){
iterator.next();
verify(iterator).next();
}
}
疑惑?心得。
注意自测的方法传递的参数会对打桩产生影响
我们打桩其实是在真实方法的内部对应的位置进行打桩的,如果真实方法的对应的真实代码不会执行,那么我们在单测的时候,打桩就无效。你比如说,我们现在想要在真实方法的if判断体里面打一个桩,如果这个if判断体一直不成立,我们我们在单元测试方法中打的桩就不会替换掉真实代码中的具体位置。解释过程如下图:
下图这个异常可以通过上图来解答:
什么叫做打桩?以及什么情况下需要打桩什么情况下不需要打桩?
打桩其实就是在真实代码的地方用一个模拟方法代替,然后真实方法执行到这个地方的时候,它的返回值是我们模拟的返回值。when().thenReturn(),用这句代码我们可以自己给某个方法设定返回值,这就叫做打桩。
在什么时候需要打桩呢?如果我们想要自测的方法有返回值,我们需要打桩,如下图:
什么时候不需要打桩呢?当我们想要测试的方法没有返回值的时候,这个时候我们就不用打桩了,但是我们需要检测一下这个方法是否执行了,如下图:
打桩的时候返回不同的结果单测的结果可能不同
比如现在有一个需求,就是我们在增加期间的时候,数据库中不能存在这个期间,要不然就增加失败,在真实方法中的代码表现如下图:
单测的代码如下图:
上次的编译可能会影响以后的代码执行
我们提示的异常信息上面还有一个LocalDateTime日期类,但是我们之前明明已经把我们的LocalDateTime日期类改成了Date日期类,那么还报我们在项目中使用了LocalDateTime日期类,这是为什么呢?可能是因为我们的工程没有重新编译,我们现在使用的还是之间没有改成Date的字节码,所以才会出现上面的异常。这个时候我们要做的就是把Parent父工程重新clean一下,然后在重新运行我们的单元测试方法。
clean完parent父工程之后,重新测试单测方法,执行成功。
公司使用Mockito单测的格式规范
首先我们的单测代码都是写在deploy工程的test包下面的,如下图:
我们写的单测类必须要继承一个类并且也要使用到一个注解,如下图:
我们单测的方法该怎么写?
比如我们举一个例子,我们举一个测试真实方法的例子。
真实方法
首先来看一下我们真实的方法是什么样子的,如下图:
单测方法
然后我们再来看一下我们单测的方法是什么样子的,如下图:
需要注意的一些细节
如下图:
总之就是,我们单测方法里面的模拟返回值必须要符合真实方法中的逻辑,要不然真实方法会报异常,我们的单测就不能通过。
真实方法也会使用我们模拟的返回值
有的时候真实方法中也需要用到我们的单测方法中模拟出来的返回值,如下图:
assert关键字的作用
assert关键字可以终止程序正常运行,assert关键字的作用是断言,如果它发现后面是true,那么程序可以正常运行,但是如果它发现后面是false,那么它会主动抛出一个异常并终止正在运行的程序。
以上是关于Mockito单元测试的主要内容,如果未能解决你的问题,请参考以下文章
单元测试ContainerRequestFilter,它使用ResourceInfo和mockito