Mock 意为模拟,指模拟一个代码所依赖的框架、第三方代码或者构建起来较为复杂的对象,将被测试的代码和其它代码隔离开来,而 Mockito 框架是一款流行的 Mock 框架。
Mock简介
当我们所写的代码相对简单时,被测试代码不会或者很少依赖其它的类和代码,但是当代码较为复杂时,被测试代码免不了于系统中其它的部分进行交互,这给测试带来很大的不便。还有另外一种情况,在测试建立初始条件时,对于一些构建比较复杂难以构建的对象,同样给测试带来一定困难。此时 Mock 框架就显示出了其作用所在。
单元测试的目的是把被测试代码和其它部分相隔离,当成一个独立的单元来进行测试,这样可以让你无视被测试单元所依赖部分运行的准确性,从而达到准确测试的目的。
Mockito 框架是一款比较流行的 Mock 框架,当前最新版是 2.18.5,但是 gradle 插件在 maven 仓库中只能下载到2.18.3 版本。其不仅可以用于 java 工程,还可以搭配 dexmaker
用于Android 项目中。类似的框架还有 EasyMock、PowerMock 等。
Android Studio 中配置 Mockito
在 Mockito 2.6.1版本开始,Mockito 团队专门为 Android 做了相关支持,为了在 Android Test 测试中使用 Mockito,需要引入以下依赖:
repositories { jcenter() } dependencies { ... testCompile 'org.mockito:mockito-core:2.18.5' androidTestCompile 'org.mockito:mockito-android:2.18.5' ... }
mockito-android:
依赖中已经包含了mockito-core
,所以不需要单独添加该依赖。
Mockito 基本用法
Mockito 框架有官方详细的说明文档,地址为 http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html
Mock 一个对象
假设有如下一个类以及一个接口:
public class Student { }public interface StudentDatabaseHelper { public Student queryStudentWithId (int id) ; public void updateNewScoreWithId (int id, int score) ; }
Serializable Mocks
@Test public void serializableMockTest () { Student student = mock(Student.class, withSettings().serializable()); }
重置 Mock
官方文档中提示不要在测试用例进行过程中使用重置 Mock。重置 Mock 使用 reset()
方法。
@Test public void resetMockTest () { Student student = mock(Student.class); doNothing().when(student).setScore(99 ); student.setScore(99 ); reset(student); }
参数匹配
当对方法进行插桩或者进行验证时,可以配合 Harmcrest 匹配器进行参数匹配,Mockito 框架集成了匹配器,并对其进行了一定的扩展,因此可以直接进行使用,不需要添加依赖。
Mockito 框架对匹配器所做的扩展的类为MockitoHamcrest
,其内部方法都是以如下形式命名的:
static xxx xxxThat (Matcher<xxx> matcher)
其中的xxx
可以为arg
、boolean
、byte
、char
以及基础数据类型,一位对某一类型的参数进行匹配。
@Test public void argMatcherTest () { ArrayList<String> mockList = mock(ArrayList.class); when(mockList.get(anyInt())).thenReturn("arg matcher0" ); assertEquals("arg matcher0" , mockList.get(0 )); assertEquals("arg matcher0" , mockList.get(4 )); when(mockList.get(intThat(i -> i > 5 ))).thenReturn("big than 5" ); assertEquals("big than 5" , mockList.get(999 )); when(mockList.indexOf(argThat((String str) -> str.length() > 5 ))).thenReturn(5 ); assertEquals(5 , mockList.indexOf("arg matcher test" )); }
除了以上匹配器以外,还可以自定义匹配器,具体请查看 Harmcrest 自定义匹配器 文章。
如果在 stub 或者 验证的过程中对某个方法使用了参数匹配,则所有的参数都必须使用参数匹配,对于确定的值需要使用eq()
匹配器。
参数捕捉
参数捕捉指对某一方法的参数进行捕捉,并对其进行验证。
@Test public void argumentCaptureTest () { Student student = mock(Student.class); ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class); student.setScore(100 ); verify(student).setScore(captor.capture()); assertEquals(100 , (int )captor.getValue()); }
Stub 插桩
对模拟的对象进行插桩主要使用方法when
,配合thenReturn
、doThrow
等方法完成。对方法进行 stub 是 Mock 框架的核心所在。
全部模拟(Mock) 和部分模拟(Spy) 进行 Stub
由于模拟一个对象的时候可以全部模拟(即 Mock),也可以部分模拟(即Spy),两者稍微有所区别。下面是以 ArraryList 进行简单的举例:
@Test (expected = IndexOutOfBoundsException.class)public void stubbingTest () { ArrayList<String> mockList = mock(ArrayList.class); when(mockList.get(0 )).thenReturn("mock stub test" ); assertEquals("mock stub test" , mockList.get(0 )); assertNull(mockList.get(1 )); ArrayList<String> arrayList = new ArrayList<>(); ArrayList<String> spyList = spy(arrayList); spyList.add("one" ); spyList.add("two" ); assertEquals("one" , spyList.get(0 )); System.out.println(spyList.get(1 )); assertNull(spyList.get(2 )); }
对 void 方法进行 stub
如果一个方法返回返回为空,则需要特定的方法进行 stub。例如:
doXXX().when(mockObj).someMethod([arg1, arg2, ...]);
其中 doXXX()
可以是doNothing()
、doCallRealMethod
、doThrow()
、doAnswer()
其中的一种:举例如下:
@Test public void stubVoidMethodTest () { StudentDatabaseHelper helper = mock(StudentDatabaseHelper.class); Object object = new Object(); doNothing().when(helper).updateNewScoreWithId(anyInt(), anyInt()); doThrow(RuntimeException.class).when(helper).updateNewScoreWithId(anyInt(), anyInt()); doAnswer(invocation -> { Object[] arguments = invocation.getArguments(); if (arguments.length == 2 ) { return arguments[0 ]; } else { return arguments[1 ]; } }).when(helper).updateNewScoreWithId(anyInt(), anyInt()); }
对同一个方法进行连续 stub
某些时候,在测试的过程中重复调用了某个方法,并且此方法是被测试单元之外需要进行 stub 的。使用之前的方法,对每次方法的调用都使用
when(mockObj.someMethod()).thenReturn("value" );
会显得代码会很累赘。Mockito 框架提供了一种简单的迭代的方法对同一个方法进行 stub。
@Test public void consecutiveStubTest () { Student mockStudent = mock(Student.class); when(mockStudent.getName()) .thenReturn("name1" ) .thenReturn("name2" ) .thenReturn("name3" ) .thenReturn("name4" ); assertEquals("name1" , mockStudent.getName()); assertEquals("name2" , mockStudent.getName()); assertEquals("name3" , mockStudent.getName()); assertEquals("name4" , mockStudent.getName()); }
为未进行 stub 的方法设置默认返回值
@Test public void setDefaultReturnValues () { Student student = mock(Student.class); when(student.getName()).thenAnswer(invocation -> "default value" ); assertEquals("default value" , student.getName()); }
验证 verify
行为验证
@Test public void verifyTest () { Student mockStudent = mock(Student.class); when(mockStudent.getName()).thenReturn("name1" ); doNothing().when(mockStudent).setScore(99 ); mockStudent.setScore(99 ); mockStudent.getName(); verify(mockStudent).setScore(99 ); verify(mockStudent).getName(); }
调用次数验证
Mockito 提供了几个常用的验证调用次数的方法,可以利用这些方法对待测试代码所调用的方法进行调用次数上的精确验证:
times(n)
- 验证具体的调用次数,n 为具体正整数,代表所需要验证的次数,在 verify 中默认验证次数为 1;
atLeast(n)
- 字面意思,验证至少调用的次数;
atLeastOnce()
- 即atLeast(1)
;
atMost(n)
- 和atLeast
相反,表示至多调用多少次;
never()
- 表示验证从未调用过该方法,等价于times(0)
;
例如:
@Test public void verifyCallTimesTest () { Student mockStudent = mock(Student.class); mockStudent.setScore(1 ); mockStudent.setScore(2 ); mockStudent.setScore(2 ); verify(mockStudent).setScore(1 ); verify(mockStudent, times(1 )).setScore(1 ); verify(mockStudent, atLeast(1 )).setScore(1 ); verify(mockStudent, times(2 )).setScore(2 ); verify(mockStudent, atMost(5 )).setScore(2 ); verify(mockStudent, atLeastOnce()).setScore(2 ); verify(mockStudent, never()).setScore(4 ); }
调用次序验证
验证调用次序主要用到了InOrder
类中的verify()
方法,举例如下:
@Test public void verifyOrderTest () { Student singleMockStudent = mock(Student.class); singleMockStudent.setScore(1 ); singleMockStudent.setScore(2 ); InOrder inOrder = Mockito.inOrder(singleMockStudent); inOrder.verify(singleMockStudent).setScore(1 ); inOrder.verify(singleMockStudent).setScore(2 ); Student firstStudent = mock(Student.class); Student secondStudent = mock(Student.class); firstStudent.setScore(3 ); secondStudent.setScore(4 ); inOrder = Mockito.inOrder(firstStudent, secondStudent); inOrder.verify(firstStudent).setScore(3 ); inOrder.verify(secondStudent).setScore(4 ); }
验证 mock 对象是否交互
验证对象是否进行交互的意思是验证测试用例中 mock 的对象在验证之前是否与之产生交互。
@Test public void verifyInteractionTest () { Student firstStudent = mock(Student.class); Student secondStudent = mock(Student.class); firstStudent.setScore(1 ); verify(firstStudent).setScore(1 ); verify(secondStudent, never()).setScore(anyInt()); verifyZeroInteractions(secondStudent); }
查找是否有未验证的交互
主要是验证对某个 mock 对象进行了操作,并且对操作进行了 verify。不介意在每个测试用例中使用,官方文档原文为:
A word of warning : Some users who did a lot of classic, expect-run-verify mocking tend to use verifyNoMoreInteractions()
very often, even in every test method. verifyNoMoreInteractions()
is not recommended to use in every test method. verifyNoMoreInteractions()
is a handy assertion from the interaction testing toolkit. Use it only when it’s relevant. Abusing it leads to overspecified , less maintainable tests. You can find further reading here .
@Test (expected = NoInteractionsWanted.class)public void noMoreInteractionsTest () { Student student = mock(Student.class); student.setScore(1 ); verifyNoMoreInteractions(student); }
超时验证
超时验证是用来验证某一特定的方法或者操作在规定的时间内能执行完毕,防止进程阻塞或者更新缓慢等,给用户带来不友好的体验。
在JUnit基本注释 一文中也提到了超时测试,利用@Test(timeout = time_with_ms)
的注释可以对整个测试用例运行的执行时长进行测试。PowerMock 同样也提供了verify
+ timeout
的方法对某一特定的方法进行验证, 但其验证的作用是在验证之前对进程阻塞一段时间,之后再去验证。例如:
被测试代码两个方法如下:
public class TimeoutDemo { public void timeoutMethod () { } }
测试代码如下:
@Test public void timeoutTest () { TimeoutDemo demo = mock(TimeoutDemo.class); doCallRealMethod().when(demo).timeoutMethod(); demo.timeoutMethod(); verify(demo,timeout(200 )).timeoutMethod(); verify(demo, timeout(200 ).times(1 )).timeoutMethod(); verify(demo,new Timeout(200 , new VerificationMode() { @Override public void verify (VerificationData data) { } @Override public VerificationMode description (String description) { return null ; } })).timeoutMethod(); }
timeout()
方法需要1.8.5 版本之后才可以使用。