1.Mockito是什么
Mockito是Mock框架,mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。
2.为什么需要Mock
测试驱动的开发( TDD)要求我们先写单元测试,再写实现代码。在写单元测试的过程中,我们往往会遇到要测试的类有很多依赖,这些依赖的类/对象/资源又有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。如下图所示:
mock1.png为了测试A,我们需要Mock B类和C类(用虚拟对象来代替)如下图所示:
mock2.png3.使用Mock对象进行测试
3.1单元测试的目标与挑战
单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关系应该尽量被消除。一个可行的消除方法是替换掉依赖类(测试替换),也就是说我们可以使用替身来替换掉真正的依赖对象。
测试类的分类如下:
-
dummy object 做为参数传递给方法但是绝对不会被使用。譬如说,这种测试类内部的方法不会被调用,或者是用来填充某个方法的参数。
-
Fake object 是真正接口或抽象类的实现体,但给对象内部实现很简单。譬如说,它存在内存中而不是真正的数据库中。(译者注:Fake 实现了真正的逻辑,但它的存在只是为了测试,而不适合于用在产品中。)
-
stub 类是依赖类的部分方法实现,而这些方法在你测试类和接口的时候会被用到,也就是说 stub 类在测试中会被实例化。stub 类会回应任何外部测试的调用。stub 类有时候还会记录调用的一些信息。
-
mock object 是指类或者接口的模拟实现,你可以自定义这个对象中某个方法的输出结果。
测试替代技术能够在测试中模拟测试类以外对象。因此你可以验证测试类是否响应正常。譬如说,你可以验证在 Mock 对象的某一个方法是否被调用。这可以确保隔离了外部依赖的干扰只测试测试类。
我们选择 Mock 对象的原因是因为 Mock 对象只需要少量代码的配置。
3.2 Mock对象的产生
你可以手动创建一个 Mock 对象或者使用 Mock 框架来模拟这些类,Mock 框架允许你在运行时创建 Mock 对象并且定义它的行为。
一个典型的例子是把 Mock 对象模拟成数据的提供者。在正式的生产环境中它会被实现用来连接数据源。但是我们在测试的时候 Mock 对象将会模拟成数据提供者来确保我们的测试环境始终是相同的。
Mock 对象可以被提供来进行测试。因此,我们测试的类应该避免任何外部数据的强依赖。
通过 Mock 对象或者 Mock 框架,我们可以测试代码中期望的行为。譬如说,验证只有某个存在 Mock 对象的方法是否被调用了。
3.3 使用Mockito生成Mock对象
Mockito 是一个流行 mock 框架,可以和JUnit结合起来使用。Mockito 允许你创建和配置 mock 对象。使用Mockito可以明显的简化对外部依赖的测试类的开发。
一般使用 Mockito 需要执行下面三步:
- 模拟并替换测试代码中外部依赖
- 执行测试代码
- 验证测试代码是否被正常执行
4.添加Mockito依赖到项目中
将以下依赖项添加到Gradle构建文件中:
dependencies {
// ... more entries
testCompile 'junit:junit:4.12'
// required if you want to use Mockito for unit tests
testCompile 'org.mockito:mockito-core:2.7.22'
// required if you want to use Mockito for Android tests
androidTestCompile 'org.mockito:mockito-android:2.7.22'
}
5.使用Mockito API
5.1 使用Mockito创建模拟对象
Mockito提供了集中创建模拟对象的方法:
- 使用静态mock()方法
- 使用@Mock注解
如果使用@Mock注解,则必须通过MockitoRule触发创建注解对象。它调用静态方法MockitoAnnotations.initMocks(this)来填充注解的字段。或者,你可以使用@RunWith(MockitoJUnitRunner.class)达到同样的目的。
以下示例说明了@Mock注释和MockitoRule规则的用法:
mock4.png- 告诉Mockito模拟databaseMock实例
- Mockito 通过 @mock 注解创建 mock 对象
- 使用已经创建的mock初始化这个类
- 在测试环境下,执行测试类中的代码
- 使用断言确保调用的方法返回值为 true
- 验证 query 方法是否被 MyDatabase 的 mock 对象调用
5.2 配置Mock
当我们需要配置某个方法的返回值的时候,Mockito 提供了链式的 API 供我们方便的调用
when(….).thenReturn(….)可以被用来定义当条件满足时函数的返回值,如果你需要定义多个返回值,可以多次定义。当你多次调用函数的时候,Mockito 会根据你定义的先后顺序来返回返回值。Mocks 还可以根据传入参数的不同来定义不同的返回值。譬如说你的函数可以将anyString 或者 anyInt作为输入参数,然后定义其特定的放回值。
使用示例如下:
public class MockitoTest {
@Test
public void testOnReturnValue() {
// 创建Mock对象
MyClass mockClass = mock(MyClass.class);
// 自定义getUniqueId返回值
when(mockClass.getUniqueId()).thenReturn(22);
// 使用mock对象测试
assertEquals(22, mockClass.getUniqueId());
}
/**
* 返回多个值
*/
@Test
public void testMoreThanOneReturnValue() {
Iterator i = mock(Iterator.class);
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 testReturnValueNotDependentOnMethodParameter() {
Comparable c = mock(Comparable.class);
when(c.compareTo(anyInt())).thenReturn(-1);
assertEquals(-1, c.compareTo(9));
}
/**
* 根据参数类型来返回值
*/
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable<Todo> c = mock(Comparable.class);
when(c.compareTo(isA(Todo.class))).thenReturn(0);
assertEquals(0, c.compareTo(new Todo(1)));
}
}
对于无返回值的函数,我们可以使用doReturn(…).when(…).methodCall来获得类似的效果。例如我们想在调用某些无返回值函数的时候抛出异常,那么可以使用doThrow 方法。如下面代码片段所示:
@Test(expected = IOException.class)
public void testReturnThrow() throws IOException {
// 创建并配置 mock 对象
OutputStream mockStream = mock(OutputStream.class);
doThrow(new IOException()).when(mockStream).close();
// 使用 mock
OutputStreamWriter streamWriter = new OutputStreamWriter(mockStream);
streamWriter.close();
}
5.3 使用 Spy 封装 java 对象
@Spy或者spy()方法可以被用来封装 java 对象。被封装后,除非特殊声明(打桩 stub),否则都会真正的调用对象里面的每一个方法:
@Test(expected = IndexOutOfBoundsException.class)
public void testLinkedListSpyWrong() {
// Lets mock a LinkedList
List<String> list = new LinkedList<>();
List<String> spy = spy(list);
// 下面代码不生效
// 真正的方法会被调用
// 将会抛出 IndexOutOfBoundsException 的异常,因为 List 为空
when(spy.get(0)).thenReturn("foo");
assertEquals("foo", spy.get(0));
}
@Test
public void testLinkedListSpyCorrect() {
// Lets mock a LinkedList
List<String> list = new LinkedList<>();
List<String> spy = spy(list);
// You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);
assertEquals("foo", spy.get(0));
}
5.4 验证 mock 对象方法是否被调用
Mockito 会跟踪 mock 对象里面所有的方法和变量。所以我们可以用来验证函数在传入特定参数的时候是否被调用。这种方式的测试称行为测试,行为测试并不会检查函数的返回值,而是检查在传入正确参数时候函数是否被调用。
@Test
public void testVerify() {
// 创建并配置 mock 对象
MyClass test = mock(MyClass.class);
when(test.getUniqueId()).thenReturn(43);
// 调用mock对象里面的方法并传入参数为12
test.testing(12);
test.getUniqueId();
test.getUniqueId();
// 验证输入参数是12时,方法是否被调用
verify(test).testing(ArgumentMatchers.eq(12));
// 其他用来验证函数是否被调用的方法
// 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");
}
5.5 使用 @InjectMocks 在 Mockito 中进行依赖注入
我们也可以使用@InjectMocks 注解来创建对象,它会根据类型来注入对象里面的成员方法和变量。假定我们有 ArticleManager 类:
public class ArticleManager {
private User user;
private ArticleDatabase database;
public ArticleManager(User user, ArticleDatabase database) {
super();
this.user = user;
this.database = database;
}
public void initialize() {
database.addListener(new ArticleListener());
}
}
这个类可以通过Mockito构建,它的依赖可以通过模拟对象来实现,如下面的代码片段所示:
@RunWith(MockitoJUnitRunner.class)
public class ArticleManagerTest {
@Mock ArticleCalculator calculator;
@Mock ArticleDatabase database;
@Mock User user;
@Spy private UserProvider userProvider = new ConsumerUserProvider();
@InjectMocks private ArticleManager manager;
@Test public void shouldDoSomething() {
// calls addListener with an instance of ArticleListener
manager.initialize();
// validate that addListener was called
verify(database).addListener(any(ArticleListener.class));
}
}
创建一个实例ArticleManager并将其注入到它中
更多的详情可以查看:
http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html
5.6 捕捉参数
ArgumentCaptor类允许验证期间访问方法调用的参数。捕获这些方法调用的参数并将其用于测试
public class ArgumentCaptorTest {
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Captor
private ArgumentCaptor<List<String>> captor;
@Test
public void testArgumentCaptor() {
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"));
}
}
5.7 对复杂的Mock使用Answers
通过Answer可以定义一个复杂的结果对象,虽然thenReturn每次都返回一个预定义的值,但是有了Answer,你可以根据stubbed方法的参数来预估响应。如果你的stubbed方法调用其中一个参数的函数,或者返回第一个参数以允许方法链的进行,那么这会很有用。后者存在一种静态方法。注意,有不同的方式来配置Answer:
import static org.mockito.AdditionalAnswers.returnsFirstArg;
@Test
public final void answerTest() {
// with doAnswer():
doAnswer(returnsFirstArg()).when(list).add(anyString());
// with thenAnswer():
when(list.add(anyString())).thenAnswer(returnsFirstArg());
// with then() alias:
when(list.add(anyString())).then(returnsFirstArg());
}
或者如果你需要对你的结果进行回调:
@Test
public final void callbackTest() {
ApiService service = mock(ApiService.class);
when(service.login(any(Callback.class))).thenAnswer(i -> {
Callback callback = i.getArgument(0);
callback.notify("Success");
return null;
});
}
甚至可以模拟像DAO这样的持久性服务,但是如果您的Answers过于复杂,您应该考虑创建一个虚拟类而不是mock
List<User> userMap = new ArrayList<>();
UserDao dao = mock(UserDao.class);
when(dao.save(any(User.class))).thenAnswer(i -> {
User user = i.getArgument(0);
userMap.add(user.getId(), user);
return null;
});
when(dao.find(any(Integer.class))).thenAnswer(i -> {
int id = i.getArgument(0);
return userMap.get(id);
});
5.8 限制
Mockito当然也有一定的限制。例如,你不能mock静态方法和私有方法。
详见:FAQ for Mockito limitations for the details
6. 使用Mockito编写单元测试
6.1 对Android 应用进行测试
一个Intent带参数传递的功能代码如下所示:
public class Util {
public static Intent createQuery(Context context, String query, String value) {
Intent i = new Intent(context, TestActivity.class);
i.putExtra("QUERY", query);
i.putExtra("VALUE", value);
return i;
}
}
6.2 app / build.gradle文件中添加Mockito依赖
dependencies {
// ... more entries
testCompile 'junit:junit:4.12'
// required if you want to use Mockito for unit tests
testCompile 'org.mockito:mockito-core:2.7.22'
// required if you want to use Mockito for Android tests
androidTestCompile 'org.mockito:mockito-android:2.7.22'
}
6.3 创建测试
在androidTest文件夹中使用Mockito创建在Android上运行的新单元测试,来验证在传递正确 extra data 的情况下,intent 是否被触发。
因此我们需要使用 Mockito 来 mock 一个Context对象,如下代码所示:
@RunWith(AndroidJUnit4.class)
public class UtilTest {
@Test
public void shouldContainTheCorrectExtras() throws Exception {
Context context = mock(Context.class);
Intent intent = Util.createQuery(context, "query", "value");
assertNotNull(intent);
Bundle extras = intent.getExtras();
assertNotNull(extras);
assertEquals("query", extras.getString("QUERY"));
assertEquals("value", extras.getString("VALUE"));
}
}
7. 实例:使用 Mockito 创建一个 mock 对象
7.1 创建一个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
}
}
7.2 模拟ITweet实例
为了能够不启动复杂的服务来得到 ITweet,我们可以使用 Mockito 来模拟得到该实例。
public class TwitterTest {
@Test
public void testSendTweet() {
TwitterClient client = new TwitterClient();
ITweet tweet = mock(ITweet.class);
when(tweet.getMessage()).thenReturn("Using mockito is great");
client.sendTweet(tweet);
verify(tweet, atLeastOnce()).getMessage();
}
}
现在 TwitterClient 可以使用 ITweet 接口的实现,当调用 getMessage() 方法的时候将会打印 "Using Mockito is great" 信息,确保 getMessage() 方法至少调用一次
7.3 测试结果
运行测试,查看代码是否测试通过。
8. 小结
这篇文章介绍了mock的概念以及Mockito的使用,可能Mockito的很多的一些其他方法没有介绍,但这只是阅读文档的问题而已,更重要的是理解mock的概念。
本文讲解了Mockito的一些局限性,下一篇文章将在本篇的基础上,讲解通过PowerMockito解决这些问题。
参考:
网友评论