Mockito的入门及使用
什么是Mockito?
作为一个新手,刚开始听到Mockito这个词,我想到了Mojito鸡尾酒,哈哈哈哈哈,不知道你有没有联想到!
开个玩笑,言归正传,到底什么是mock对象?什么是Mockito呢?其实Mockito 是一个强大的用于 Java 开发的模拟测试框架, 通过 Mockito 我们可以创建和配置 Mock 对象, 进而简化有外部依赖的类的测试.
先来看看下面的这个例子
我们如果要对A进行测试的话,就必须对BC测试,而对B测试就必须对他的分支DE进行测试,所以就会很麻烦,如果DE没写完,那么B就会很难测试。要测试的目标类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。
这时就提出了mock对象,先看看下图
将B和C所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。这样无论B、C有多少分支依赖对象,说通俗点,就是把他们分支依赖看成一个整体,而我作为一个测试者只关心我要测试的对象A所对应的分支对象就可以了。
Mockito API文档使用
Mockito是一套非常强大的测试框架,被广泛的应用于Java程序的unit test中。相比于EasyMock框架,Mockito使用起来简单,学习成本很低,而且具有非常简洁的API,测试代码的可读性很高。
Mockito的使用,有详细的api文档,具体可以查看:https://javadoc.io/static/org.mockito/mockito-core/3.4.4/org/mockito/Mockito.html, 下面是整理的一些常用的使用方式。
Mockito具体使用
1、添加依赖
在 build gradle (Moudle app)的dependencies里添加依赖
testImplementation "org.mockito:mockito-core:+"
androidTestImplementation "org.mockito:mockito-android:+"
2、创建二个类
Person类
package com.example.testdemo;
public class Person {
private final int id;
private final String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
PersonDAO 接口
package com.example.testdemo;
public interface PersonDao {
Person getPerson(int id);
boolean update(Person person);
}
PersonService类
package com.example.testdemo;
public class PersonService {
private final PersonDao personDao;
public PersonService(PersonDao personDao) {
this.personDao = personDao;
}
public boolean update(int id, String name) {
Person person = personDao.getPerson(id);
if (person == null) {
return false;
}
Person personUpdate = new Person(person.getId(), name);
return personDao.update(personUpdate);
}
}
我们在开发过程中有可能PersonDAO 接口还没写好,但是我们想测试一下PersonService类里的update方法,要怎么办呢?等接口写好,我们再测试?不,这样也太蠢了,哈哈哈,这时候就可以通过mock来mock一个实例来做测试。
那么我们可以在对PersonService类进行Go To Test,如果有不懂的可以去看
https://blog.csdn.net/weixin_45552475/article/details/107481416
这里面有如何快速创建一个Test类。这里就不贴图了。
打开PersonServiceTest类,写测试代码,这里先贴出来代码,下面再慢慢讲解下。
package com.example.testdemo;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
public class PersonServiceTest {
private PersonDao mockDao;
private PersonService personService;
@Before
public void setUp() throws Exception {
//模拟PersonDao对象
mockDao = Mockito.mock(PersonDao.class);
//当调用mockDao.getPerson(1)时返回一个id为1,name为"Person1"的Person对象。
Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1, "Person1"));
Mockito.when(mockDao.update(isA(Person.class))).thenReturn(true);
personService = new PersonService(mockDao);
}
@Test
public void testUpdate() throws Exception {
boolean result = personService.update(1, "new name");
assertTrue("is true", result);
//验证是否执行过一次getPerson(1)
Mockito.verify(mockDao, times(1)).getPerson(eq(1));
//验证是否执行过一次update
Mockito.verify(mockDao, times(1)).update(isA(Person.class));
}
@Test
public void testUpdateNotFind() throws Exception {
boolean result = personService.update(2, "new name");
assertFalse("must true", result);
//验证是否执行过一次getPerson(1)
Mockito.verify(mockDao, times(1)).getPerson(eq(1));
//验证是否执行过一次update
Mockito.verify(mockDao, never()).update(isA(Person.class));
}
}
-
Step1:在setUp中,首先我们先模拟一个mockDao对象出来,主要通过Mockito.mock(PersonDAO.class);来mock的。
-
Step2:添加Stubbind条件,调用Mockito的when(xxxx).thenReturn(yyyy);方法,意思就是指定当执行了xxxx方法的时候,返回 thenReturn 的yyyy值,相当于是对模拟对象的配置过程,为某些条件给定一个预期的返回值。
打桩需要注意以下两点:
1、对于 static 和 final 方法, Mockito 无法对其when(…).thenReturn(…) 操作。
2、当我们连续两次为同一个方法使用 stub 的时候,他只会只用最新的一次。
- 我们这里的Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,“Jim”)); 意思是当调用mockDao.getPerson(1)时返回一个id为1,name为"Jim"的Person对象。
Mockito.when(mockDao.update(isA(Person.class))).thenReturn(true);意思是当调用update方法时,返回true。 - Step3:new一个PersonService对象。
- Step4:在testUpdate,
Mockito.verify(mockDao, times(1)).getPerson(eq(1));
验证是否执行过一次getPerson(1)。只要有执行过Mockito都会记录下。
Mockito.verify(mockDao, times(1)).update(isA(Person.class));
验证是否执行过一次update
Mockito常用方法
验证行为(verify验证)
一旦创建,mock会记录所有交互,你可以验证所有你想要验证的东西,即使删掉也会有操作记录在。
@Test
public void testVerify() throws Exception {
//mock creation
List mockList = Mockito.mock(List.class);
mockList.add("one");
mockList.add("two");
mockList.add("two");
mockList.clear();
//验证是否调用过一次 mockedList.add("one")方法,若不是(0次或者大于一次),测试将不通过,默认是一次
Mockito.verify(mockList).add("one");
//验证调用过2次 mockedList.add("two")方法,若不是,测试将不通过
Mockito.verify(mockList,Mockito.times(2)).add("two");
//验证是否调用过一次 mockedList.clear()方法,若没有(0次或者大于一次),测试将不通过
Mockito.verify(mockList).clear();
}
这里主要注意。mock会记录你所有的操作的,即使删除也会记录下来。比如mockList中添加完,然后clear掉,Mockito.verify(mockList).add(“one”);这个的验证也是会通过的,验证的关键方法是verify, verify有两个重载方法:
verify(T mock): 默认是验证调用一次,里面默认调用times(1)。 verify(T mock,VerificationMode mode):mode,调用次数.
Stubbing 条件
@Test
public void testStubbing() throws Exception{
//你可以mock具体的类,而不仅仅是接口
LinkedList mockedList = Mockito.mock(LinkedList.class);
//设置值
Mockito.when(mockedList.get(0)).thenReturn("one");
Mockito.when(mockedList.get(1)).thenReturn("two");
Mockito.when(mockedList.get(2)).thenReturn(new RuntimeException());
//print 输出"one"
System.out.println(mockedList.get(0));
//输出 "java.lang.RuntimeException"
System.out.println(mockedList.get(2));
//这里会打印 "null" 因为 get(999) 没有设置
System.out.println(mockedList.get(999));
Mockito.verify(mockedList).get(0);
}
对于stubbing,有以下几点需要注意:
- 对于有返回值的方法,mock会默认返回null、空集合、默认值。比如,为int/Integer返回0,为boolean/Boolean返回false
- stubbing可以被覆盖,但是请注意覆盖已有的stubbing有可能不是很好
- 一旦stubbing,不管调用多少次,方法都会永远返回stubbing的值
- 当你对同一个方法进行多次stubbing,最后一次stubbing是最重要的
参数匹配(ArgumentMatcher)
@Test
public void testArgumentMatcher() throws Exception {
LinkedList mockedList = Mockito.mock(LinkedList.class);
//用内置的参数匹配器来stub
Mockito.when(mockedList.get(Mockito.anyInt())).thenReturn("element");
//打印 "element"
System.out.println(mockedList.get(999));
//你也可以用参数匹配器来验证,此处测试通过
Mockito.verify(mockedList).get(Mockito.anyInt());
//此处测试将不通过,因为没调用get(33)
Mockito.verify(mockedList).get(Mockito.eq(33));
}
注:如果你使用了参数匹配器,那么所有参数都应该使用参数匹配器
verify(mock).someMethod(anyInt(), anyString(), eq(“third argument”));
//上面是正确的,因为eq返回参数匹配器
verify(mock).someMethod(anyInt(), anyString(), “third argument”);
//上面将会抛异常,因为第三个参数不是参数匹配器,一旦使用了参数匹配器来验证,那么所有参数都应该使用参数匹配
验证准确调用次数(InvocationTimes)
/**
* 验证准确的调用次数,最多、最少、从未等
* @throws Exception
*/
@Test
public void testInvocationTimes() throws Exception {
LinkedList mockedList = Mockito.mock(LinkedList.class);
//using mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//下面两个是等价的, 默认使用times(1)
Mockito.verify(mockedList).add("once");
Mockito.verify(mockedList, Mockito.times(1)).add("once");
//验证准确的调用次数
Mockito.verify(mockedList, Mockito.times(2)).add("twice");
Mockito.verify(mockedList, Mockito.times(3)).add("three times");
//从未调用过. never()是times(0)的别名
Mockito.verify(mockedList, Mockito.never()).add("never happened");
//用atLeast()/atMost()验证
Mockito.verify(mockedList, Mockito.atLeastOnce()).add("three times");
Mockito.verify(mockedList, Mockito.atLeast(2)).add("three times");
//最多
Mockito.verify(mockedList, Mockito.atMost(3)).add("three times");
}
为void方法抛异常
@Test
public void testVoidMethodsWithExceptions() throws Exception {
LinkedList mockedList = Mockito.mock(LinkedList.class);
Mockito.doThrow(new RuntimeException()).when(mockedList).clear();
//这边会抛出异常
mockedList.clear();
}
验证调用顺序(InOrder)
@Test
public void testVerificationInOrder() throws Exception {
List singleMock = Mockito.mock(List.class);
//使用单个mock对象
singleMock.add("was added first");
singleMock.add("was added second");
//创建inOrder
InOrder inOrder = Mockito.inOrder(singleMock);
//验证调用次数,若是调换两句,将会出错,因为singleMock.add("was added first")是先调用的
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
// 多个mock对象
List firstMock = Mockito.mock(List.class);
List secondMock = Mockito.mock(List.class);
//using mocks
firstMock.add("was called first");
secondMock.add("was called second");
//创建多个mock对象的inOrder
inOrder = Mockito.inOrder(firstMock, secondMock);
//验证firstMock先于secondMock调用
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
}
spy监视真正的对象
spy是创建一个拷贝,如果你保留原始的list,并用它来进行操作,那么spy并不能检测到其交互
@Test
public void testSpy() throws Exception {
List list = new LinkedList();
List spy = Mockito.spy(list);
//可选的,你可以stub某些方法
Mockito.when(spy.size()).thenReturn(100);
//如果操作原始list,那么spy是不会检测到的。
list.add("first");
//调用"真正"的方法
spy.add("one");
spy.add("two");
//打印one
System.out.println(spy.get(0));
//size()方法被stub了,打印100
System.out.println(spy.size());
//可选,验证spy对象的行为
Mockito.verify(spy).add("one");
Mockito.verify(spy).add("two");
//下面写法有问题,spy.get(10)会抛IndexOutOfBoundsException异常
Mockito.when(spy.get(10)).thenReturn("foo");
//可用以下方式
Mockito.doReturn("foo").when(spy).get(10);
}
Captur 参数捕捉
@Test
public void testCapturingArguments() throws Exception {
List mockedList = Mockito.mock(List.class);
ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
mockedList.add("John");
//进行参数捕捉,这里参数应该是"John"
Mockito.verify(mockedList).add(argument.capture());
assertEquals("John",argument.getValue());
}
验证mock对象没有产生过交互
@Test
public void testInteractionNeverHappened() {
List mockOne = mock(List.class);
List mockTwo = mock(List.class);
//测试通过
verifyZeroInteractions(mockOne, mockTwo);
mockOne.add("");
//测试不通过,因为mockTwo已经发生过交互了
verifyZeroInteractions(mockOne, mockTwo);
}
@Mock注解
使用@Mock注解来定义mock对象有如下的优点:
方便mock对象的创建、减少mock对象创建的重复代码、提高测试代码可读性、变量名字作为mock对象的标示,所以易于排错
我们还是通过第一个例子来修改:
public class MockTest {
@Mock
private PersonDAO mockDao;
private PersonService personService;
@Before
public void setUp() throws Exception {
/**
* 要想让Annotation起作用,就必须初始化.一般初始化都在@Before里面
*/
MockitoAnnotations.initMocks(this);
Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,"Jim"));
Mockito.when(mockDao.update(Mockito.isA(Person.class))).thenReturn(true);
personService = new PersonService(mockDao);
}
@Test
public void testUpdate() throws Exception {
boolean result = personService.update(1,"Tom");
assertTrue("is true",result);
//验证是否执行过一次getPerson(1)
Mockito.verify(mockDao,Mockito.times(1)).getPerson(Mockito.eq(1));
//验证是否执行过一次update
Mockito.verify(mockDao,Mockito.times(1)).update(Mockito.isA(Person.class));
}
}
@Captor注解
@Captor是参数捕获器的注解,通过注解的方式可以更便捷的对ArgumentCaptor进行定义。还可以通过ArgumentCaptor对象的forClass(Class clazz)方法来构建ArgumentCaptor对象,然后便可在验证时对方法的参数进行捕获,最后验证捕获的参数值。如果方法有多个参数都要捕获验证,那就需要创建多个ArgumentCaptor对象处理。
public class MockTest {
@Captor
private ArgumentCaptor<String> captor;
@Before
public void setUp() throws Exception {
/**
* 要想让Annotation起作用,就必须初始化.一般初始化都在@Before里面
*/
MockitoAnnotations.initMocks(this);
}
@Test
public void testCaptor() throws Exception {
/**
* ArgumentCaptor的Api
argument.capture() 捕获方法参数;
argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值;
argument.getAllValues() 方法进行多次调用后,返回多个参数值;
*/
list.add("John");
//进行参数捕捉,这里参数应该是"John"
Mockito.verify(list).add(captor.capture());
assertEquals("John",captor.getValue());
}
}
@Spy注解
使用@Spy生成的类,所有方法都是真实方法,返回值和真实方法一样的,是使用Mockito.spy()的快捷方式.
public class MockTest {
@Spy
private List list = new LinkedList();
@Before
public void setUp() throws Exception {
/**
* 要想让Annotation起作用,就必须初始化.一般初始化都在@Before里面
*/
MockitoAnnotations.initMocks(this);
}
@Test
public void testSpy() throws Exception {
//可选的,你可以stub某些方法
Mockito.when(list.size()).thenReturn(100);
//调用"真正"的方法
list.add("one");
list.add("two");
//打印one
System.out.println(list.get(0));
//size()方法被stub了,打印100
System.out.println(list.size());
}
}
参见资源:
http://blog.csdn.net/shensky711/article/details/52771493
https://www.jianshu.com/p/2759c5e88b7e