本文参考自两篇文档,全面梳理了Mocito的使用。添加了一些自己的理解,并做了格式整理:
Mockito是当前最流行的单元测试Mock框架。采用Mock框架,我们可以虚拟出一个外部依赖,降低测试组件之间的耦合度,只注重代码的流程与结果,真正地实现测试目的。Mock三剑客中,MockServer主要是模拟外部服务,Mockito目的是模拟被测试类所依赖的项目中的其他类,而MockMVC 则专注于简化 Controller 接口的测试。
1. 使用 mock 对象来进行测试
1.1 单元测试的目标和挑战
单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关系应该尽量被消除。
一个可行的消除方法是替换掉依赖类(测试替换),也就是说我们可以使用替身来替换掉真正的依赖对象。
1.2 测试类的分类
- dummy object:会作为参数传递给方法,但是绝对不会被使用。譬如说,这种测试类内部的方法不会被调用,或者是用来填充某个方法的参数。
- Fake: 是真正接口或抽象类的实现体,但对象内部实现很简单。譬如说,它存在内存中而不是真正的数据库中。(译者注:Fake 实现了真正的逻辑,但它的存在只是为了测试,而不适合于用在产品中。)
- stub:这种类实现了依赖类的部分方法,这些方法在测试类和接口的时候会被用到,也就是说 stub 类在测试中会被实例化。stub 类会回应任何外部测试的调用。stub 类有时候还会记录调用的一些信息。
- mock object: 这种类是指依赖类或者接口的模拟实现,你可以自定义这个对象中某个方法的输出结果。
测试替代技术能够在测试中模拟测试类以外对象。因此可以验证测试类是否响应正常。譬如说,你可以验证在 Mock 对象的某一个方法是否被调用。这可以确保隔离了外部依赖的干扰只测试测试类。
选择 Mock 对象的原因是因为 Mock 对象只需要少量代码的配置。
1.3 Mock 对象的产生
可以手动创建一个 Mock 对象或者使用 Mock 框架来模拟这些类,Mock 框架允许在运行时创建 Mock 对象并且定义它的行为。
一个典型的例子是把 Mock 对象模拟成数据的提供者。在正式的生产环境中它会被实现用来连接数据源。但是在测试的时候 ,Mock 对象将会模拟成数据提供者来确保测试环境始终是相同的。
Mock 对象可以被提供来进行测试。因此,我们测试的类应该避免任何外部数据的强依赖。
通过 Mock 对象或者 Mock 框架,可以测试代码中期望的行为。譬如说,验证只有某个存在 Mock 对象的方法是否被调用了。
1.4 使用 Mockito 生成 Mock 对象
Mockito 是一个流行 mock 框架,可以和 JUnit 结合起来使用。Mockito 允许你创建和配置 mock 对象。使用 Mockito 可以明显的简化对外部依赖的测试类的开发。
一般使用 Mockito 需要执行下面三步
- a.模拟并替换测试代码中外部依赖
- b.执行测试代码
- c.验证测试代码是否被正确的执行
2. 为自己的项目添加 Mockito 依赖
2.1 在 Gradle 添加 Mockito 依赖
如果使用 Gradle 构建,将下面代码加入 Gradle 的构建文件中为自己项目添加 Mockito 依赖
repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:2.0.57-beta" }
2.2 在 Maven 添加 Mockito 依赖
需要在 Maven 声明依赖,将下面代码加入到pom.xml文件中。
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
3. 使用 Mockito API
3.1 静态引用
如果在代码中静态引用了org.mockito.Mockito.*;,那你就可以直接调用静态方法和静态变量而不用创建对象,譬如直接调用 mock() 方法。
3.2 Junit5中Mockito的初始化
在JUnit 5中,@Rule注解已被 @ExtendWith 和 TestRule 接口所取代。因此,要在JUnit 5中使用Mockito,有以下两种方法来实现:
- 方法1:使用
@MockitoExtension注解
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
// 统一初始化Mockito对象
@ExtendWith(MockitoExtension.class)
public class ExampleTest {
@Mockprivate
SomeClass mockSomeClass;
@Testpublic
void testSomething() {
// 使用mock对象,设置期望
when(mockSomeClass.someMethod()).thenReturn("mocked result");
// 运行测试逻辑
}
}
在这个示例中,@ExtendWith(MockitoExtension.class)注解用于指定使用 MockitoExtension 扩展来初始化Mockito。这样,使用 @Mock 注解标记的 mock 对象将会在每个测试方法之前自动初始化。
- 方法2: 使用
MockitoAnnotations.openMocks()方法
如果要在测试类中手动初始化mock对象,可以使用 MockitoAnnotations.openMocks()方法,示例如下:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
public class ExampleTest {
@Mockprivate
SomeClass mockSomeClass;
@BeforeEach
public void setUp() {
// 手动初始化mock对象
MockitoAnnotations.openMocks(this);
}
@Testpublic
void testSomething() {
// 在测试中使用mock对象,设置期望
when(mockSomeClass.someMethod()).thenReturn("mocked result");
// 运行测试逻辑
}
}
这两种方式都可以在JUnit 5中使用Mockito来初始化mock对象。
3.3 使用 Mockito 创建和配置 mock 对象
除了上面所说的使用 mock() 静态方法外,Mockito 还支持通过 @Mock 注解的方式来创建 mock 对象。
如果使用注解,那么必须要实例化 mock 对象。Mockito 在遇到使用注解的字段的时候,会调用MockitoAnnotations.initMocks(this) 来初始化该 mock 对象。另外也可以通过使用@RunWith(MockitoJUnitRunner.class)来达到相同的效果。
通过下面的例子可以了解到使用@Mock 的方法和MockitoRule规则。
import static org.mockito.Mockito.*;
public class MockitoTest {
@Mock
MyDatabase databaseMock; (1)
@BeforeEach (2)
public void setUp() {
// 手动初始化mock对象
MockitoAnnotations.openMocks(this);
}
@Test
public void testQuery() {
ClassToTest t = new ClassToTest(databaseMock); (3)
boolean check = t.query("* from t"); (4)
assertTrue(check); (5)
verify(databaseMock).query("* from t"); (6)
}
}
代码中的标记点说明如下:
- (1)告诉 Mockito 模拟 databaseMock 实例,Mockito 通过 @mock 注解创建 mock 对象;
- (2)在每次运行测试方法之前初始化 Mockito 的模拟对象;
- (3)使用已经创建的 mock 初始化这个准备去测试的类
- (4)在测试环境下,执行测试类中的代码。因为实例 t 是通过真实的构造函数创建的,而不是通过 Mockito 创建的模拟对象,所以执行的就是被测试类的实际方法。
- (5)使用断言确保调用的方法返回值为 true
- (6)验证 query 方法是否被 MyDatabase 的 mock 对象调用
3.4 配置 mock
当配置某个方法的返回值的时候,Mockito 提供了链式的 API:
when(….).thenReturn(….)可以被用来定义当条件满足时函数的返回值。如果你需要定义多个返回值,可以多次定义。当多次调用函数的时候,Mockito 会根据定义的先后顺序来返回返回值。
Mocks 还可以根据传入参数的不同来定义不同的返回值。譬如说函数可以将anyString 或者 anyInt作为输入参数,然后定义其特定的放回值。
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
public class MyClassTest {
@Test
public void test1() {
// 创建 mock
MyClass test = Mockito.mock(MyClass.class);
// 模拟 MyClass类中 getUniqueId()方法的返回值
when(test.getUniqueId()).thenReturn(43);
// 在测试中使用mock对象
assertEquals(test.getUniqueId(), 43);
}
// 返回多个值
@Test
public void testMoreThanOneReturnValue() {
Iterator i= mock(Iterator.class);
// 第一次调用 i.next() 会返回 "Mockito"
// 第二次调用 i.next() 会返回 "rocks"。
// 以后的调用都会返回 "rocks"。
when(i.next()).thenReturn("Mockito").thenReturn("rocks");
String result=i.next()+" "+i.next();
// 断言
assertEquals("Mockito rocks", result);
}
// 如何根据输入来返回值
@Test
public void testReturnValueDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo("Mockito")).thenReturn(1);
when(c.compareTo("Eclipse")).thenReturn(2);
// 断言
assertEquals(1,c.compareTo("Mockito"));
}
// 如何让返回值不依赖于输入
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo(anyInt())).thenReturn(-1);
// 断言
assertEquals(-1 ,c.compareTo(9));
}
// 根据参数类型来返回值
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable c= mock(Comparable.class);
when(c.compareTo(isA(Todo.class))).thenReturn(0);
// 断言
Todo todo = new Todo(5);
assertEquals(todo ,c.compareTo(new Todo(1)));
}
}
对于无返回值的函数,可以使用doReturn(…).when(…).methodCall来获得类似的效果。
如想在调用某些无返回值函数的时候抛出异常,那么可以使用doThrow 方法。如下面代码片段所示:
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
// 下面测试用例描述了如何使用doThrow()方法
@Test(expected=IOException.class)
public void testForIOException() {
// 创建并配置 mock 对象
OutputStream mockStream = mock(OutputStream.class);
doThrow(new IOException()).when(mockStream).close();
// 使用 mock
OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream);
streamWriter.close();
}
3.5 验证 mock 对象方法是否被调用
Mockito 会跟踪 mock 对象里面所有的方法和变量。所以可以用来验证函数在传入特定参数的时候是否被调用。 这种方式的测试称“行为测试”,行为测试并不会检查函数的返回值,而是检查在传入正确参数时候,函数是否被调用。 Java import static org.mockito.Mockito.*;
@Test public void testVerify() { // 创建并配置 mock 对象 MyClass test = Mockito.mock(MyClass.class); when(test.getUniqueId()).thenReturn(43);
// 调用mock对象里面的方法并传入参数为12
test.testing(12);
test.getUniqueId();
test.getUniqueId();
// 查看在传入参数为12的时候方法是否被调用
verify(test).testing(Matchers.eq(12));
// 方法是否被调用两次
verify(test, times(2)).getUniqueId();
// 其他用来验证函数是否被调用的方法
verify(mock, never()).someMethod("never called");
verify(mock, atLeastOnce()).someMethod("called at least once");
verify(mock, atLeast(2)).someMethod("called at least twice");
verify(mock, times(5)).someMethod("called five times");
verify(mock, atMost(3)).someMethod("called at most 3 times");
}
3.6 使用 Spy 封装 java 对象
@Spy 或者spy()方法可以被用来封装 java 对象。
被封装后,除非特殊声明(打桩 stub),否则都会真正的调用对象里面的每一个方法
import static org.mockito.Mockito.*;
// Lets mock a LinkedList
List list = new LinkedList();
List spy = spy(list);
// 可用 doReturn() 来打桩
doReturn("foo").when(spy).get(0);
// 下面代码不生效
// 真正的方法会被调用将会抛出 IndexOutOfBoundsException 的异常,因为 List 为空
when(spy.get(0)).thenReturn("foo");
方法verifyNoMoreInteractions()允许检查没有其他的方法被调用。
3.7 使用 @InjectMocks 自动注入对象中的方法和变量
可以使用@InjectMocks 注解来创建对象,它会根据类型来注入对象里面的成员方法和变量。
假定我们有 ArticleManager 类:
public class ArticleManager {
private User user;
private ArticleDatabase database;
ArticleManager(User user) {
this.user = user;
}
void setDatabase(ArticleDatabase database) { }
}
这个类会被 Mockito 构造,而类的成员方法和变量都会被 mock 对象所代替,正如下面的代码片段所示:
public class ArticleManagerTest {
@Mock ArticleCalculator calculator;
@Mock ArticleDatabase database;
@Most User user;
@Spy private UserProvider userProvider = new ConsumerUserProvider();
// 创建 ArticleManager 实例,并用Mock对象替代这个实例中的方法和变量
@InjectMocks private ArticleManager manager; (1)
@Test
public void shouldDoSomething() {
// 假定 ArticleManager 有一个叫 initialize() 的方法被调用了
// 而initialize方法内使用 ArticleListener 来调用 addListener 方法
manager.initialize();
// 验证 addListener 方法被调用
verify(database).addListener(any(ArticleListener.class));
}
}
3.8 捕捉参数
ArgumentCaptor类允许在 verification 期间访问方法的参数。得到方法的参数后我们可以使用它进行测试。
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.util.Arrays;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
public class MockitoTests {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Captor
private ArgumentCaptor<List<String>> captor;
@Test
public final void shouldContainCertainListItem() {
List<String> asList = Arrays.asList("someElement_test", "someElement");
final List<String> mockedList = mock(List.class);
mockedList.addAll(asList);
verify(mockedList).addAll(captor.capture());
final List<String> capturedArgument = captor.getValue();
assertThat(capturedArgument, hasItem("someElement"));
}
}
3.9 Mockito 的限制
Mockito 使用时有一定的限制,下面三种数据类型不能够被测试:
- final classes
- anonymous classes
- primitive types
4. 在 Android 中使用 Mockito
在 Android 中的 Gradle 构建文件中,加入 Mockito 依赖后就可以直接使用 Mockito 了。
若想使用 Android Instrumented tests 的话,还需要添加 dexmaker 和 dexmaker-mockito 依赖到
Gradle 的构建文件中。(需要 Mockito 1.9.5 版本以上)
dependencies {
testCompile 'junit:junit:4.12'
// Mockito unit test 的依赖
testCompile 'org.mockito:mockito-core:1.+'
// Mockito Android instrumentation tests 的依赖
androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
}
5. 使用实例
5.1 实例:使用 Mockito 写一个 Instrumented Unit Test
5.1.1 创建一个测试的 Android 应用
创建一个包名为com.vogella.android.testing.mockito.contextmock的 Android 应用,添加一个静态方法 ,方法里面创建一个包含参数的 Intent,如下代码所示:
public static Intent createQuery(Context context, String query, String value) {
// 简单起见,重用MainActivity
Intent i = new Intent(context, MainActivity.class);
i.putExtra("QUERY", query);
i.putExtra("VALUE", value);
return i;
}
5.1.2 在 app/build.gradle 文件中添加 Mockito 依赖
dependencies {
// Mockito 和 JUnit 的依赖
// instrumentation unit tests on the JVM
androidTestCompile 'junit:junit:4.12'
androidTestCompile 'org.mockito:mockito-core:2.0.57-beta'
androidTestCompile 'com.android.support.test:runner:0.3'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
// Mockito 和 JUnit 的依赖
// tests on the JVM
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.+'
}
5.1.3 创建测试
使用 Mockito 创建一个单元测试来验证在传递正确 extra data 的情况下,intent 是否被触发。
因此我们需要使用 Mockito 来 mock 一个Context对象,如下代码所示:
package com.vogella.android.testing.mockitocontextmock;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class TextIntentCreation {
@Test
public void testIntentShouldBeCreated() {
Context context = Mockito.mock(Context.class);
Intent intent = MainActivity.createQuery(context, "query", "value");
assertNotNull(intent);
Bundle extras = intent.getExtras();
assertNotNull(extras);
assertEquals("query", extras.getString("QUERY"));
assertEquals("value", extras.getString("VALUE"));
}
}
5.2 实例:使用 Mockito 创建一个 mock 对象
5.2.1 目标
创建一个 Api,它可以被 Mockito 来模拟并做一些工作
5.2.2 创建一个 Twitter API 的例子
实现 TwitterClient类,它内部使用到了 ITweet 的实现。但是ITweet实例很难得到,譬如说他需要启动一个很复杂的服务来得到。
public interface ITweet {
String getMessage();
}
public class TwitterClient {
public void sendTweet(ITweet tweet) {
String message = tweet.getMessage();
// send the message to Twitter
}
}
5.2.3 模拟 ITweet 的实例
为了能够不启动复杂的服务来得到 ITweet,我们可以使用 Mockito 来模拟得到该实例。
@Test
public void testSendingTweet() {
TwitterClient twitterClient = new TwitterClient();
ITweet iTweet = mock(ITweet.class);
when(iTweet.getMessage()).thenReturn("Using mockito is great");
twitterClient.sendTweet(iTweet);
}
现在 TwitterClient 可以使用 ITweet 接口的实现,当调用 getMessage() 方法的时候将会打印 "Using Mockito is great" 信息。
5.2.4 验证方法调用
确保 getMessage() 方法至少调用一次。
@Test
public void testSendingTweet() {
TwitterClient twitterClient = new TwitterClient();
ITweet iTweet = mock(ITweet.class);
when(iTweet.getMessage()).thenReturn("Using mockito is great");
twitterClient.sendTweet(iTweet);
verify(iTweet, atLeastOnce()).getMessage();
}
5.2.5 验证
运行测试,查看代码是否测试通过。
6. 模拟静态方法
6.1 使用 Powermock 来模拟静态方法
因为 Mockito 不能够 mock 静态方法,因此我们可以使用 Powermock。
import java.net.InetAddress;
import java.net.UnknownHostException;
public final class NetworkReader {
public static String getLocalHostname() {
String hostname = "";
try {
InetAddress addr = InetAddress.getLocalHost();
// Get hostname
hostname = addr.getHostName();
} catch ( UnknownHostException e ) {
}
return hostname;
}
}
我们模拟了 NetworkReader 的依赖,如下代码所示:
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
@RunWith( PowerMockRunner.class )
@PrepareForTest( NetworkReader.class )
public class MyTest {
// 测试代码
@Test
public void testSomething() {
mockStatic( NetworkUtil.class );
when( NetworkReader.getLocalHostname() ).andReturn( "localhost" );
// 与 NetworkReader 协作的测试
}
6.2 用封装的方法代替 Powermock
有时候我们可以在静态方法周围包含非静态的方法来达到和 Powermock 同样的效果。
class FooWraper {
void someMethod() {
Foo.someStaticMethod()
}
}