单元测试利器Mockito框架
什么是Mock
Mock
的中文译为仿制的,模拟的,虚假的。对于测试框架来说,即构造出一个模拟/虚假的对象,使我们的测试能顺利进行下去。
Mock
测试就是在测试过程中,对于某些
不容易构造
(如
HttpServletRequest
必须在
Servlet
容器中才能构造出来)或者不容易获取
比较复杂
的对象(如
JDBC
中的
ResultSet
对象),用一个
虚拟
的对象(
Mock
对象)来创建,以便测试方法。
为什么使用Mock测试
单元测试 是为了验证我们的代码运行正确性,我们注重的是代码的流程以及结果的正确与否。
对比真实运行代码,可能其中有一些 外部依赖 的构建步骤相对麻烦,如果我们还是按照真实代码的构建规则构造出外部依赖,会大大增加单元测试的工作,代码也会参杂太多非测试部分的内容,测试用例显得复杂难懂。
采用
Mock
框架,我们可以
虚拟
出一个
外部依赖
,只注重代码的
流程与结果
,真正地实现测试目的。
Mock测试框架的好处
- 可以很简单的虚拟出一个复杂对象(比如虚拟出一个接口的实现类);
-
可以配置
mock
对象的行为; - 可以使测试用例只注重测试流程与结果;
- 减少外部类、系统和依赖给单元测试带来的耦合。
Mockito的流程
如图所示,使用
Mockito
的大致流程如下:
-
创建
外部依赖
的
Mock
对象, 然后将此Mock
对象注入到 测试类 中; - 执行 测试代码 ;
- 校验 测试代码 是否执行正确。
Mockito的使用
在
Module
的
build.gradle
中添加如下内容:
dependencies {
//Mockito for unit tests
testImplementation "org.mockito:mockito-core:2.+"
//Mockito for Android tests
androidTestImplementation 'org.mockito:mockito-android:2.+'
}
这里稍微解释下:
-
mockito-core
: 用于 本地单元测试 ,其测试代码路径位于module-name/src/test/java/
-
mockito-android
: 用于 设备测试 ,即需要运行android
设备进行测试,其测试代码路径位于module-name/src/androidTest/java/
mockito-core最新版本可以在 Maven 中查询:mockito-core。 mockito-android最新版本可以在 Maven 中查询:mockito-android
Mockito的使用示例
普通单元测试使用
mockito(mockito-core)
,路径:
module-name/src/test/java/
这里摘用官网的
Demo
:
检验调对象相关行为是否被调用
import static org.mockito.Mockito.*;
// Mock creation
List mockedList = mock(List.class);
// Use mock object - it does not throw any "unexpected interaction" exception
mockedList.add("one"); //调用了add("one")行为
mockedList.clear(); //调用了clear()行为
// Selective, explicit, highly readable verification
verify(mockedList).add("one"); // 检验add("one")是否已被调用
verify(mockedList).clear(); // 检验clear()是否已被调用
这里
mock
了一个
List
(这里只是为了用作
Demo
示例,通常对于
List
这种简单的类对象创建而言,直接
new
一个真实的对象即可,无需进行
mock
),
verify()
会检验对象是否在前面已经执行了相关行为,这里
mockedList
在
verify
之前已经执行了
add("one")
和
clear()
行为,所以
verify()
会通过。
配置/方法行为
// you can mock concrete classes, not only interfaces
LinkedList mockedList = mock(LinkedList.class);
// stubbing appears before the actual execution
when(mockedList.get(0)).thenReturn("first");
// the following prints "first"
System.out.println(mockedList.get(0));
// the following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
这里对几个比较重要的点进行解析:
when(mockedList.get(0)).thenReturn("first")
这句话
Mockito
会解析为:当对象
mockedList
调用
get()
方法,并且参数为
0
时,返回结果为
"first"
,这相当于定制了我们
mock
对象的行为结果(
mock LinkedList
对象为
mockedList
,指定其行为
get(0)
,则返回结果为
"first"
)。
mockedList.get(999)
由于
mockedList
没有指定
get(999)
的行为,所以其结果为
null
。因为
Mockito
的底层原理是使用
cglib
动态生成一个
代理类对象
,因此,
mock
出来的对象其实质就是一个
代理
,该代理在
没有配置/指定行为
的情况下,默认返回
空值
。
上面的
Demo
使用的是
静态方法
mock()
模拟出一个实例,我们还可以通过注解
@Mock
也模拟出一个实例:
@Mock
private Intent mIntent;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void mockAndroid(){
Intent intent = mockIntent();
assertThat(intent.getAction()).isEqualTo("com.yn.test.mockito");
assertThat(intent.getStringExtra("Name")).isEqualTo("Whyn");
private Intent mockIntent(){
when(mIntent.getAction()).thenReturn("com.yn.test.mockito");
when(mIntent.getStringExtra("Name")).thenReturn("Whyn");
return mIntent;
}
对于标记有
@Mock
,
@Spy
,
@InjectMocks
等注解的成员变量的
初始化
到目前为止有
2
种方法:
-
对
JUnit
测试类添加@RunWith(MockitoJUnitRunner.class)
-
在标示有
@Before
方法内调用初始化方法:MockitoAnnotations.initMocks(Object)
上面的测试用例,对于
@Mock
等注解的成员变量的初始化又多了一种方式
MockitoRule
。规则
MockitoRule
会自动帮我们调用
MockitoAnnotations.initMocks(this)
去
实例化
出
注解
的成员变量,我们就无需手动进行初始化了。
Mockito的重要方法
实例化虚拟对象
// You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class);
// Stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
// Following prints "first"
System.out.println(mockedList.get(0));
// Following throws runtime exception
System.out.println(mockedList.get(1));
// Following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));
// Although it is possible to verify a stubbed invocation, usually it's just redundant
// If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
// If your code doesn't care what get(0) returns, then it should not be stubbed. Not convinced? See here.
verify(mockedList).get(0);
-
对于所有方法,
mock
对象默认返回null
, 原始类型/原始类型包装类 默认值,或者 空集合 。比如对于int/Integer
类型,则返回0
,对于boolean/Boolean
则返回false
。 -
行为配置(
stub
)是可以被复写的:比如通常的对象行为是具有一定的配置,但是测试方法可以复写这个行为。请谨记行为复写可能表明潜在的行为太多了。 - 一旦配置了行为,方法总是会返回 配置值 ,无论该方法被调用了多少次。
- 最后一次行为配置是更加重要的,当你为一个带有相同参数的相同方法配置了很多次,最后一次起作用。
参数匹配
Mockito
通过参数对象的
equals()
方法来验证参数是否一致,当需要更多的灵活性时,可以使用参数匹配器:
// Stubbing using built-in anyInt() argument matcher
when(mockedList.get(anyInt())).thenReturn("element");
// Stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
// Following prints "element"
System.out.println(mockedList.get(999));
// You can also verify using an argument matcher
verify(mockedList).get(anyInt());
// Argument matchers can also be written as Java 8 Lambdas
verify(mockedList).add(argThat(someString -> someString.length() > 5));
参数匹配器
允许更加灵活的
验证
和
行为配置
。更多
内置匹配器
和
自定义参数匹配器
例子请参考:
ArgumentMatchers
,
MockitoHamcrest
注意:如果使用了参数匹配器,那么所有的参数都需要提供一个参数匹配器。
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// Above is correct - eq() is also an argument matcher
verify(mock).someMethod(anyInt(), anyString(), "third argument");
// Above is incorrect - exception will be thrown because third argument is given without an argument matcher.
类似
anyObject()
,
eq()
这类匹配器并不返回匹配数值。他们内部记录一个
匹配器堆栈
并返回一个空值(通常为
null
)。这个实现是为了匹配
java
编译器的
静态类型安全
,这样做的后果就是你不能在
检验/配置方法
外使用
anyObject()
,
eq()
等方法。
校验次数
LinkedList mockedList = mock(LinkedList.class);
// Use mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
// Follow two verifications work exactly the same - times(1) is used by default
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
// Exact number of invocations verification
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
// Verification using never(). never() is an alias to times(0)
verify(mockedList, never()).add("never happened");
// Verification using atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");
校验次数方法常用的有如下几个:
MethodMeaningtimes(n)次数为n,默认为1(times(1))never()次数为0,相当于times(0)atLeast(n)最少n次atLeastOnce()最少一次atMost(n)最多n次
抛出异常
doThrow(new RuntimeException()).when(mockedList).clear();
// following throws RuntimeException
mockedList.clear();
按顺序校验
有时对于一些行为,有先后顺序之分,所以,当我们在校验时,就需要考虑这个行为的先后顺序:
// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);
// Use a single mock
singleMock.add("was added first");
singleMock.add("was added second");
// Create an inOrder verifier for a single mock
InOrder inOrder = inOrder(singleMock);
// Following will make sure that add is first called with "was added first, then with "was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
// B. Multiple mocks that must be used in a particular order
List firstMock = mock(List.class);
List secondMock = mock(List.class);
// Use mocks
firstMock.add("was called first");
secondMock.add("was called second");
// Create inOrder object passing any mocks that need to be verified in order
InOrder inOrder = inOrder(firstMock, secondMock);
// Following will make sure that firstMock was called before secondMock
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
存根连续调用
对于同一个方法,如果我们想让其在 多次调用 中分别 返回不同 的数值,那么就可以使用存根连续调用:
when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");
// First call: throws runtime exception:
mock.someMethod("some arg");
// Second call: prints "foo"
System.out.println(mock.someMethod("some arg"));
// Any consecutive call: prints "foo" as well (last stubbing wins).
System.out.println(mock.someMethod("some arg"));
也可以使用下面更简洁的存根连续调用方法:
when(mock.someMethod("some arg")).thenReturn("one", "two", "three");
注意:存根连续调用要求必须使用链式调用,如果使用的是同个方法的多个存根配置,那么只有最后一个起作用(覆盖前面的存根配置)。
// All mock.someMethod("some arg") calls will return "two"
when(mock.someMethod("some arg").thenReturn("one")
when(mock.someMethod("some arg").thenReturn("two")
无返回值函数
对于
返回类型
为
void
的方法,存根要求使用另一种形式的
when(Object)
函数,因为编译器要求括号内不能存在
void
方法。
例如,存根一个返回类型为
void
的方法,要求调用时抛出一个异常:
doThrow(new RuntimeException()).when(mockedList).clear();
// Following throws RuntimeException:
mockedList.clear();
监视真实对象
前面使用的都是
mock
出来一个对象。这样,当
没有配置/存根
其具体行为的话,结果就会返回
空类型
。而如果使用
特务对象
(
spy
),那么对于
没有存根
的行为,它会调用
原来对象
的方法。可以把
spy
想象成局部
mock
。
List list = new LinkedList();
List spy = spy(list);
// Optionally, you can stub out some methods:
when(spy.size()).thenReturn(100);
// Use the spy calls *real* methods
spy.add("one");
spy.add("two");
// Prints "one" - the first element of a list
System.out.println(spy.get(0));
// Size() method was stubbed - 100 is printed
System.out.println(spy.size());
// Optionally, you can verify
verify(spy).add("one");
verify(spy).add("two");
注意:由于 spy 是局部 mock,所以有时候使用 when(Object) 时,无法做到存根作用。此时,就可以考虑使用 doReturn() | Answer() | Throw() 这类方法进行存根:
List list = new LinkedList();
List spy = spy(list);
// Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
when(spy.get(0)).thenReturn("foo");
// You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);
spy
并不是
真实对象
的
代理
。相反的,它对传递过来的
真实对象
进行
克隆
。所以,对
真实对象
的任何操作,
spy
对象并不会感知到。同理,对
spy
对象的任何操作,也不会影响到
真实对象
。
当然,如果使用
mock
进行对象的
局部
mock
,通过
doCallRealMethod() | thenCallRealMethod()
方法也是可以的:
// You can enable partial mock capabilities selectively on mocks:
Foo mock = mock(Foo.class);
// Be sure the real implementation is 'safe'.
// If real implementation throws exceptions or depends on specific state of the object then you're in trouble.
when(mock.someMethod()).thenCallRealMethod();
测试驱动开发
以
行为驱动开发
的格式使用 //given //when //then 注释为测试用法基石编写测试用例,这正是
Mockito
官方编写测试用例方法,强烈建议使用这种方式测试编写。
import static org.mockito.BDDMockito.*;
Seller seller = mock(Seller.class);
Shop shop = new Shop(seller);
public void shouldBuyBread() throws Exception {
// Given
given(seller.askForBread()).willReturn(new Bread());
// When
Goods goods = shop.buyBread();
// Then
assertThat(goods, containBread());
}
自定义错误校验输出信息
// Will print a custom message on verification failure
verify(mock, description("This will print on failure")).someMethod();
// Will work with any verification mode
verify(mock, times(2).description("someMethod should be called twice")).someMethod();
如果对软件测试、接口测试、自动化测试、面试经验交流。感兴趣可以加软件测试交流:1085991341,还会有同行一起技术交流。
@InjectMock
构造器,方法,成员变量依赖注入 使用
@InjectMock
注解时,
Mockito
会检查
类构造器
,
方法
或
成员变量
,依据它们的
类型
进行自动
mock
。
public class InjectMockTest {
@Mock
private User user;
@Mock
private ArticleDatabase database;
@InjectMocks
private ArticleManager manager;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void testInjectMock() {
// Calls addListener with an instance of ArticleListener
manager.initialize();
// Validate that addListener was called
verify(database).addListener(any(ArticleListener.class));
public static 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());
public static class User {
public static class ArticleListener {
public static class ArticleDatabase {
public void addListener(ArticleListener listener) {
}
成员变量
manager
类型为
ArticleManager
,它的上面标识别了
@InjectMocks
。这意味着要
mock
出
manager
,
Mockito
需要先自动
mock
出
ArticleManager
所需的
构造参数
(即:
user
和
database
),最终
mock
得到一个
ArticleManager
,赋值给
manager
。
参数捕捉
ArgumentCaptor
允许在
verify
的时候获取
方法参数内容
,这使得我们能在
测试过程
中能对
调用方法参数
进行
捕捉
并
测试
。
@Rule
public MockitoRule mockitoRule = 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);