本篇文章将对Mockito重要的API进行梳理.
另外, GItHub上有相应的翻译好的中文文档: https://github.com/hehonghui/mockito-doc-zh/blob/master/README.md#0
搭建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'
}
使用Mockito创建mock对象
Mockito提供几种创建mock对象的方法:
- 使用静态方法 mock()
- 使用注解 @Mock 标注
如果使用@Mock注解, 必须去触发所标注对象的创建. 可以使用 MockitoRule来实现. 它调用了静态方法MockitoAnnotations.initMocks(this) 去初始化这个被注解标注的字段.或者也可以使用@RunWith(MockitoJUnitRunner.class).
JUnit Rule请回顾之前文章
具体的用法可以参照下面示例:
import static org.mockito.Mockito.*;
public class ClassToTestTest {
@Mock
MyDatabase databaseMock;//①
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();//②
@Test
public void query() throws Exception {
ClassToTest t = new ClassToTest(databaseMock);//③
boolean check = t.query("* from t");//④
assertTrue(check);//⑤
verify(databaseMock).query("* from t");//⑥
}
}
- 通知Mockito模拟databaseMock实例
- 通知Mockito创建被 @Mock 注解标注 的模拟对象(本例中就是databaseMock)
- 用上一步创建的模拟对象去实例化测试类
- 执行测试代码
- 断言返回值为true
- 验证MyDatabase中的query方法被调用
再次重申一下静态导入的重要性,使用静态导入可以非常好的提高代码的可读性,你值得拥有
配置模拟对象
Mockito可以通过自然的API来实现模拟对象的返回值.没有指定的方法调用返回空值:
- object返回null
- 数值类型返回0
- boolean返回false
- 集合将返回空集合
- ......
以下的断言语句仅用于演示目的. 真正的测试应该用模拟对象来测试另一些功能.
"when thenReturn"和"when thenThrow"
模拟对象可以根据传入方法中的参数来返回不同的值, when(….).thenReturn(….)方法是用来根据特定的参数来返回特定的值.
我们也可以使用像anyString或者anyInt 这样的方法来定义某个依赖数据类型的方法返回特定的值.
如果指定了多个值,他们将按照顺序返回多个值.
请参照下方示例:
@Test
public void test1() {
// 创建mock对象
MyClass test = mock(MyClass.class);
// 定义getUniqueId()方法返回特定的值
when(test.getUniqueId()).thenReturn(43);
// 执行测试
assertEquals(test.getUniqueId(), 43);
}
// 返回多个值的示例
@Test
public void testMoreThanOneReturnValue() {
Iterator<String> i= mock(Iterator.class);
when(i.next()).thenReturn("Mockito").thenReturn("rocks");
String result= i.next()+" "+i.next();
//assert
assertEquals("Mockito rocks", result);
}
// 如何根据输入来返回值
@Test
public void testReturnValueDependentOnMethodParameter() {
Comparable<String> c= mock(Comparable.class);
when(c.compareTo("Mockito")).thenReturn(1);
when(c.compareTo("Eclipse")).thenReturn(2);
//assert
assertEquals(1, c.compareTo("Mockito"));
}
// 返回值独立于输入值
@Test
public void testReturnValueInDependentOnMethodParameter() {
Comparable<Integer> c= mock(Comparable.class);
when(c.compareTo(anyInt())).thenReturn(-1);
//assert
assertEquals(-1, c.compareTo(9));
}
// 根据提供参数的类型返回特定的值
@Test
public void testReturnValueInDependentOnMethodParameter2() {
Comparable<Todo> c= mock(Comparable.class);
when(c.compareTo(isA(Todo.class))).thenReturn(0);
//assert
assertEquals(0, c.compareTo(new Todo(1)));
}
when(….).thenReturn(….)也可以用来抛出异常
Properties properties = mock(Properties.class);
when(properties.get("Anddroid")).thenThrow(new IllegalArgumentException(...));
try {
properties.get("Anddroid");
fail("Anddroid is misspelled");
} catch (IllegalArgumentException ex) {
// good!
}
"doReturn when" 和 "doThrow when"
doReturn(…).when(…)的方法调用和when(….).thenReturn(….)类似.对于调用过程中抛出的异常非常有用.而doThrow则也是它的一个变体.
具体在Spy中使用.
使用Spy包装Java对象
可以使用@Spy注解 或者 spy() 方法来包装一个真实的对象. 除非有特殊的指定,否则每次调用都会委托给该对象.
示例如下:
public class SpyTest {
@Test
public void testLinkedListSpyWrong() {
// 让我们来模拟一个LinkedList
List<String> list = new LinkedList<>();
List<String> spy = spy(list);
// spy.get(0)将会调用真实的方法
// 将会抛出 IndexOutOfBoundsException (list是空的)
when(spy.get(0)).thenReturn("foo");
assertEquals("foo", spy.get(0));
}
@Test
public void testLinkedListSpyCorrect() {
// 让我们来模拟一个LinkedList
List<String> list = new LinkedList<>();
List<String> spy = spy(list);
// 必须使用doReturn来插桩
doReturn("foo").when(spy).get(0);
assertEquals("foo", spy.get(0));
}
}
注: 在使用Spy包装真实对象时使用when(….).thenReturn(….)将无效,必须使用 doReturn(…).when(…)来进行插桩.
验证模拟对象的调用
Mockito将会追踪所有方法的调用和传入模拟对象的参数.你可以在模拟对象上使用verify()方法验证指定的条件是否满足.例如,你可以验证是否使用某些参数调用了方法.这种测试称为行为测试.行为测试并不能检查方法调用的结果,但是它可以验证一个方法是否使用正确的参数被调用.
示例如下:
public class VerifyTest {
@Test
public void testVerify() {
// 创建模拟对象
MyClass test = Mockito.mock(MyClass.class);
when(test.getUniqueId()).thenReturn(43);
// 调用模拟对象的方法testing,并传入参数12
test.testing(12);
test.getUniqueId();
test.getUniqueId();
// 检查方法testing是否使用参数
//12调用了
verify(test).testing(ArgumentMatchers.eq(12));
// 验证调用两次getUniqueId
verify(test, times(2)).getUniqueId();
// 也可以使用下面的方法来替代调用的次数
verify(test, never()).someMethod("never called 从来没有调用");
verify(test, atLeastOnce()).someMethod("called at least once 至少被调用一次");
verify(test, atLeast(2)).someMethod("called at least twice 至少被调用5次");
verify(test, times(5)).someMethod("called five times 被调用5次");
verify(test, atMost(3)).someMethod("called at most 3 times 至多被调用3次");
//下面的方法用来检查是否所有的用例都涵盖了,如果没有将测试失败
//放在所有的测试后面
verifyNoMoreInteractions(test);
}
}
如果你并不关心输入值,可以使用anyXXX()方法,例如,anyInt(), anyString()或者any(Your.class)等等方法.
@InjectMocks进行依赖注入
我们可以使用@InjectMocks注解根据类型对构造方法,普通方法和字段进行依赖注入.
假设你有下面的类.
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;
@InjectMocks
private ArticleManager manager; //①
@Test
public void shouldDoSomething() {
//使用了一个ArticleListener实例调用了addListener
manager.initialize();
// 验证database调用使用了ArticleListener类型的参数调用了addListener
verify(database).addListener(any(ArticleListener.class));
}
}
- 这一步创建了一个ArticleManager实例并注入到了模拟对象中.
Mockito可以通过构造方法注入,setter注入和属性注入的顺序来注入模拟对象(mock).因此如果ArticleManager的构造方法只包含User, 并且这两个字段都有setter,那这种情况下只有User的模拟对象会被注入.
捕获参数
ArgumentCaptor类允许在验证的时候可以访问到方法的调用参数,并用于测试.
下面的示例需要添加依赖: https://mvnrepository.com/artifact/org.hamcrest/hamcrest-library
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"));
}
}
Answer的使用
在写测试用例时针对复杂的方法结果往往会使用Answer.虽然使用thenReturn可以每次返回一个预定义的值,但是通过answers可以让你的插桩方法(stubbed method)根据参数计算出结果.
例如,下面是使用Answer实现插桩方法返回第一个参数值的示例:
//假设存在这么一个类(仅为测试,毫无意义)
class TestObj {
public String add(String firstArg, String lastArg) {
return "";
}
}
//...
@Test
public final void answerTest() {
TestObj testObj = mock(TestObj.class);
// with doAnswer():
doAnswer(returnsFirstArg()).when(testObj).add(anyString(), anyString());
// with thenAnswer():
when(testObj.add(anyString(), anyString())).thenAnswer(returnsFirstArg());
// with then() alias:
when(testObj.add(anyString(), anyString())).then(returnsFirstArg());
//测试打印结果
System.out.println(testObj.add("FirstArg", "LastArg"));
}
打印结果:
FirstArg
有的时候你可能需要一个回调作为方法参数:
@Test
public final void callbackTest() {
ApiService service = mock(ApiService.class);
when(service.login(any(Callback.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Callback callback = invocation.getArgument(0);
callback.notify("Success");
return "Test Result";
}
});
String result = service.login(new Callback() {
@Override
public void notify(String notify) {
System.out.println(notify);
}
});
System.out.println(result);
}
打印结果:
Success
Test Result
甚至可以模拟一个持久服务,比如Dao, 但是如果Answers非常复杂应该考虑创建一个fake 类而不是mock.
@Test
public final void TestDao() {
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);
});
}
模拟 final class
自从Mockito v2 以来可以模拟final class, 这个功能目前正在优化阶段,并且默认是停用的.要想激活final class,在src/test/resources/mockito-extensions/或者src/mockito-extensions/目录创建名为org.mockito.plugins.MockMaker的文件,并在文件中添加一行:
mock-maker-inline
如图所示:
测试代码:
final class FinalClass {
public final String finalMethod() { return "something"; }
}
@Test
public final void mockFinalClassTest() {
FinalClass instance = new FinalClass();
FinalClass mock = mock(FinalClass.class);
when(mock.finalMethod()).thenReturn("that other thing");
assertNotEquals(mock.finalMethod(), instance.finalMethod());
}
当然,如果你不这么做,编译器将会抛出异常:
Mockito cannot mock/spy because :
– final class
Mockito一些重要的Api暂时就介绍到这里, 更多Api请移步: https://github.com/hehonghui/mockito-doc-zh/blob/master/README.md#0