大厂的 App 可能都有一个特点,那就是依赖错综复杂,神龙见首不见尾。至今仍感叹公司的 App 还可以打出包并且能欢快的跑起来!【惊叹脸】模块化是模块化了,但这样就给我们写测试用例造成了困难:数据库连不上?服务起不来?依赖找不到?
莫担心,祭出我们的大杀器 Mock !
什么是 Mock测试?
Mock测试就是在测试过程对于那些不易构造或不易获取的对象,用一个虚假的对象来代替。目前 java 中比较流行的 Mock测试框架有 Mockito, PowerMock, easyMock, Mockito 便是我们这次要学习的内容。
Mockito 依赖
使用 Mockito 是很简单的,直接在 build.gradle 中添加
testCompile 'org.mockito:mockito-core:2.10.0'复制代码
更详细的内容可以访问 Mockito官网
小试牛刀
环境配置好了,是时候编造点需求啦!
public class StaffManage {
private StaffDao mStaffDao;
public StaffManage(StaffDao staffDao){
this.mStaffDao = staffDao;
}
public int getStaffCount(){
return mStaffDao.getStaffCount();
}
public void addStaff(Staff staff){
mStaffDao.addStaff(staff);
}
}复制代码
这是我们的员工管理类,该类负责和数据库打交道。满心欢喜的打算跑一跑时,却返现 StaffDao 还没有就绪!
class StaffDao {
public int getStaffCount() {
throw new UnsupportedOperationException();
}
public void addStaff(Staff staff) {
throw new UnsupportedOperationException();
}
}复制代码
这就略显尴尬了,如果只有普通的单元测试肯定是通不过的。那么如果 StaffDao 要拖很久才能开发完成,在这种情况下我们怎么对我们的代码进行验证呢?
public class StaffManageTest {
private StaffManage mStaffManage;
//我们通过使用 mock 方法虚拟一个 StaffDao 对象
private StaffDao staffDao = Mockito.mock(StaffDao.class);
@Before
public void setUp(){
this.mStaffManage = new StaffManage(staffDao);
}
@Test
public void getStaffCount() throws Exception {
//当虚拟对象调用 getStaffCount() 方法时就返回 200
Mockito.when(staffDao.getStaffCount()).thenReturn(200);
//我们可以通过返回值验证函数是否正确
Assert.assertEquals(200, mStaffManage.getStaffCount());
}
@Test
public void addStaff() throws Exception {
Staff staff = new Staff();
mStaffManage.addStaff(staff);
//无返回值时,可以通过断言方法的调用来验证
Mockito.verify(staffDao).addStaff(staff);
}
}复制代码
以上我们就在 StaffDao 未开发完成的情况下,完成了对 StaffManage 的功能验证。
当然 Mockito 的功能远不止如此,接下来一起看看还有那些功能?
Mockito 功能集合
在之后我们要有 “桩” 的概念,在测试的时候我们是不能改变现有的代码的,那么我们通过什么判断代码时候正确执行呢?我们利用 Mock 的对象代替真实的对象,插入到代码中,观察虚拟对象的状态达到检测的目的,也就是 “插桩”。
验证一些行为
对于一些没有返回值的方法,我们不能直接通过验证返回值来判断方法的正确与否。此时验证一些关键方法的调用便成了我们唯一的选择。
// mock 虚拟对象
List mockList = mock(List.class);
// 使用虚拟对象
mockList.add("one");
mockList.clear();
// 验证方法的调用
verify(mockList).add("one");
verify(mockList).clear();复制代码
怎么构造返回值
由于 Mock对象 并不会真的执行方法中的代码,所以如果未指定返回值的话会返回默认值,比如 int 时返回 0,boolean 时返回 false, 非基本数据时会返回 null.
//不止接口,任何类都可以通过 mock 构造虚拟对象
LinkedList linkedList = mock(LinkedList.class);
when(linkedList.get(0)).thenReturn("one");
when(linkedList.get(1)).thenReturn(new IndexOutOfBoundsException());
System.out.println(linkedList.get(0)); // one
System.out.println(linkedList.get(1)); // java.lang.IndexOutOfBoundsException
System.out.println(linkedList.get(2)); // null复制代码
参数匹配
有时候我们并不关心参数的细节,只需参数满足一定的条件即可,这个时候我们可以用参数适配器来做更灵活的配置。
linkedList.add("element");
// anyInt() 任何整数我们都返回 element
when(linkedList.get(anyInt())).thenReturn("element");
System.out.print(linkedList.get(10));
// 同样可以用在断言处,标识是否该方法被调用了,并且传入 int 类型
verify(linkedList).get(anyInt());
// 通过 ArgumentMatcher 我们可以拿到该模拟方法接受的参数
verify(linkedList).add(argThat((ArgumentMatcher<String>) argument -> argument.length() == 7));复制代码
验证被调用的确切的次数
在一些黑盒方法中,我们可能需要方法确切的被调用的次数,或者确定不被调用。Mockito 为我们提供了很好的支持。
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
// 默认情况 跟 times(1) 是一样的
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
// 可以指定确切的次数
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
// never() 相当于 times(0)
verify(mockedList, never()).add("never happened");
// 还有 atLeast() 和 atMost() 提供更灵活的验证
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");复制代码
在调用方法时抛出异常
doThrow(new RuntimeException()).when(mockedList).clear();
//following throws RuntimeException:
mockedList.clear();复制代码
验证调用的顺序
有时候单单验证方法的调用是不够的,我们还需要确定方法是不是按照正确的顺序调用。
List singleMock = mock(List.class);
singleMock.add("was added first");
singleMock.add("was added second");
// 每个 mock 对象对应一个 inOrder
InOrder inOrder = inOrder(singleMock);
// 验证调用顺序
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");复制代码
确定有些模拟对象没有交互过
用于验证一些 Mock对象 在之前的交互过程中,没有被交互过。
//using mocks - only mockOne is interacted
mockOne.add("one");
//ordinary verification
verify(mockOne).add("one");
//verify that method was never called on a mock
verify(mockOne, never()).add("two");
//verify that other mocks were not interacted
verifyZeroInteractions(mockTwo, mockThree);复制代码