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() // 或者 mavenCenter(), aliyun仓库等
}
dependencies {
...
testCompile 'org.mockito:mockito-core:2.18.5'
androidTestCompile 'org.mockito:mockito-android:2.18.5' // 不能和 inline mock maker 一起使用
...
}

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);
}
  • 使用mock方法:

    mock()方法是最基本的 mock 一个对象的方法,无论是类、接口都可以进行模拟,具体使用方法如下:

    Student student = mock(Student.class);
    StudentDatabaseHelper databaseHelper = mock(StudentDatabaseHelper.class);
  • 使用@Mock注释:

    使用@Mock注释有以下几个有点:

    • 减少代码量;
    • 增加代码的可读性;
    • 让verify出错信息更易读,因为变量名可用来描述标记mock对象;

    具体使用方法如下:

    @Mock private Student student;
    @Mock private StudentDatabaseHelper databaseHelper;
    ...
    @Before
    public void setUp() {
    MockitoAnnotations.initMocks(this); // 很重要,也可以放在基类或者 Runner 中
    }
  • 使用Spy方法或者@Spy注释:

    SpyMock的主要区别在于Mock模拟的对象是全部模拟,必须显式的对类或接口中的方法进行 stub;而 Spy则是部分模拟,其对显式 stub 的方法执行 stub 结果,但是对没有显式 stub 的方法则执行原方法。使用方法如下:

    @Spy Student student = new Student(); 
    //上面的模拟方法可以简写成
    @Spy Student student; // Mockito 会自动实例化
    // 以下是用 spy 方法模拟
    Student student = new Student();
    Student spyStudent = spy(student);
  • 使用@InjectMocks注释:

    @InjectMocks Student; // Mockito 会自动实例化该类

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可以为argbooleanbytechar以及基础数据类型,一位对某一类型的参数进行匹配。

@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));

// 可以写 lambda 表达式
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 对象
ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);

student.setScore(100);

// 先验证再捕捉参数
verify(student).setScore(captor.capture());
assertEquals(100, (int)captor.getValue());
}

Stub 插桩

对模拟的对象进行插桩主要使用方法when,配合thenReturndoThrow等方法完成。对方法进行 stub 是 Mock 框架的核心所在。

全部模拟(Mock) 和部分模拟(Spy) 进行 Stub

由于模拟一个对象的时候可以全部模拟(即 Mock),也可以部分模拟(即Spy),两者稍微有所区别。下面是以 ArraryList 进行简单的举例:

@Test(expected = IndexOutOfBoundsException.class)
public void stubbingTest() {
// mock 一个对象
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)); // 因为 mockList.get(1) 方法没有插桩,所以返回 null;

// spy 一个对象
ArrayList<String> arrayList = new ArrayList<>();
ArrayList<String> spyList = spy(arrayList);

spyList.add("one");
spyList.add("two");

assertEquals("one", spyList.get(0));
// 下面语句会输出 two.
System.out.println(spyList.get(1));
// spyList.get(2) 会抛出 IndexOutOfBoundsException
assertNull(spyList.get(2));
}

对 void 方法进行 stub

如果一个方法返回返回为空,则需要特定的方法进行 stub。例如:

doXXX().when(mockObj).someMethod([arg1, arg2, ...]);

其中 doXXX()可以是doNothing()doCallRealMethoddoThrow()doAnswer()其中的一种:举例如下:

@Test
public void stubVoidMethodTest() {
StudentDatabaseHelper helper = mock(StudentDatabaseHelper.class);
Object object = new Object();

// 执行该 stub 的方法时不做任何事
doNothing().when(helper).updateNewScoreWithId(anyInt(), anyInt());

// 执行该 stub 的方法时执行该方法真正的方法体,由于本方法是 abstract 的,所以此处在编译时会报错
// doCallRealMethod().when(helper).updateNewScoreWithId(anyInt(), anyInt());

// 执行该 stub 的方法时抛出一个异常
doThrow(RuntimeException.class).when(helper).updateNewScoreWithId(anyInt(), anyInt());

// 执行该 stub 的方法时执行特定的操作
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);

// 利用 thenAnswer() 设置默认返回值
when(student.getName()).thenAnswer(invocation -> "default value");

assertEquals("default value", student.getName());
}

验证 verify

行为验证

@Test
public void verifyTest() {
Student mockStudent = mock(Student.class);

// stub 方法
when(mockStudent.getName()).thenReturn("name1");
doNothing().when(mockStudent).setScore(99);

// 调用 stub 之后的方法
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 inOrder = Mockito.inOrder(singleMockStudent);

// 验证参数为 1 的方法先于参数为 2 的方法运行
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 对象,参数为需要进行验证调用次序的 mock 对象
inOrder = Mockito.inOrder(firstStudent, secondStudent);

// verify
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());

// 交互验证 secondStudent 从未进行交互
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);

// 验证失败,抛出 NoInteractionsWanted 异常
verifyNoMoreInteractions(student);
}

超时验证

超时验证是用来验证某一特定的方法或者操作在规定的时间内能执行完毕,防止进程阻塞或者更新缓慢等,给用户带来不友好的体验。

JUnit基本注释一文中也提到了超时测试,利用@Test(timeout = time_with_ms)的注释可以对整个测试用例运行的执行时长进行测试。PowerMock 同样也提供了verify + timeout的方法对某一特定的方法进行验证, 但其验证的作用是在验证之前对进程阻塞一段时间,之后再去验证。例如:

被测试代码两个方法如下:

public class TimeoutDemo {
public void timeoutMethod() {
// do something here
}
}

测试代码如下:

@Test
public void timeoutTest() {
TimeoutDemo demo = mock(TimeoutDemo.class);

doCallRealMethod().when(demo).timeoutMethod();
demo.timeoutMethod();


// 延迟 200ms 再验证该方法是否执行过
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) {
// custom verify mode
}

@Override
public VerificationMode description(String description) {
return null;
}
})).timeoutMethod();
}

timeout()方法需要1.8.5 版本之后才可以使用。