Android单元测试之Mockito
背景
在写单元测试的过程中,一个很普遍的问题是,要测试的目标类会有很多依赖,这些依赖的类/对象/资源又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。
Mock就是解决的方案。简单地说就是对测试的类所依赖的其他类和对象,进行mock - 构建它们的一个假的对象,定义这些假对象上的行为,然后提供给被测试对象使用。被测试对象像使用真的对象一样使用它们。用这种方式,我们可以把测试的目标限定于被测试对象本身,就如同在被测试对象周围做了一个划断,形成了一个尽量小的被测试目标。
Mockito是什么
Mockito是一套非常强大的测试框架,被广泛的应用于Java程序的unit test中。相比于EasyMock框架,Mockito使用起来简单,学习成本很低,而且具有非常简洁的API,测试代码的可读性很高。
Mockito使用
配置依赖:
testCompile "org.mockito:mockito-core:1.10.19"
先来看看Mockito的基础使用。比如我们有以下几个类:
public class Person {
private int id;
private String name;
public Person(int id,String name){
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public interface PersonDAO {
Person getPerson(int id);
boolean update(Person person);
}
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);
}
}
说明: 以上是开发中基础的mvc分层结构,比如在开发中,PersonDAO的具体实现还未完成,这时候就可以通过mock来mock一个实例来做测试。
来看一下测试时怎么写的,这里我们主要对PersonService 中的update方法写测试用例。
public class PersonServiceTest {
private PersonDAO mockDao;
private PersonService personService;
@Before
public void setUp() throws Exception {
//模拟PersonDao对象
mockDao = Mockito.mock(PersonDAO.class);
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));
}
@Test
public void testUpdateNotFind() throws Exception {
boolean result = personService.update(2, "new name");
assertFalse("must true", result);
//验证是否执行过一次getPerson(2)
Mockito.verify(mockDao, Mockito.times(1)).getPerson(Mockito.eq(2));
//验证是否执行过一次update
Mockito.verify(mockDao, Mockito.never()).update(Mockito.isA(Person.class));
}
}
简单说明一下:
- 首先在setUp中,我们先模拟一个对象出来,主要通过Mockito.mock(PersonDAO.class);来mock的。
- 然后添加Stubbind条件,Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,"Jim")); 意思是当调用mockDao.getPerson(1)时返回一个id为1,name为"Jim"的Person对象。
- 在testUpdate()方法中 Mockito.verify(mockDao,Mockito.times(1)).getPerson(Mockito.eq(1));验证是否执行过一次getPerson(1)。只要有执行过Mockito都会记录下拉,所以这句是对的。
Mockito基础使用
Mockito的使用,有详细的api文档,具体可以查看:http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html, 下面是整理的一些常用的使用方式。
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);
}
- 对于有返回值的方法,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));
}
InvocationTimes验证准确的调用次数
验证准确的调用次数包括最多、最少、从未等,times(),never(),atLeast(),atMost().
/**
* 验证准确的调用次数,最多、最少、从未等
* @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 的 Annotation,
Mockito跟junit4一样也支持Annotation,Mockito支持的注解有:@Mock,@Spy(监视真实的对象),@Captor(参数捕获器),@InjectMocks(mock对象自动注入)。
Annotation的初始化
在使用Annotation注解之前,必须先初始化,一般初始化在Junit4的@Before里面,初始化的方法为:MockitoAnnotations.initMocks(testClass)参数testClass是你所写的测试类。
@Before
public void setUp() throws Exception {
/**
* 要想让Annotation起作用,就必须初始化.一般初始化都在@Before里面
*/
MockitoAnnotations.initMocks(this);
}
@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));
}
}
结果和前面没用注解的一样。
@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());
}
}
@Captor注解
@Captor是参数捕获器的注解,通过注解的方式可以更便捷的对ArgumentCaptor进行定义。还可以通过ArgumentCaptor对象的forClass(Class<T> 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());
}
}