单元测试利器Mockito框架详解(超详细~)
前言
Mockito 是当前最流行的 单元测试 Mock 框架。采用 Mock 框架,我们可以 虚拟 出一个 外部依赖,降低测试 组件 之间的 耦合度,只注重代码的 流程与结果,真正地实现测试目的。
正文
什么是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");

校验次数方法常用的有如下几个:
|Method |Meaning| |:------|:-------| |times(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();
@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); -
mockedList.addAll(asList); -
verify(mockedList).addAll(captor.capture()); // When verify,you can capture the arguments of the calling method -
final List<String> capturedArgument = captor.getValue(); -
assertThat(capturedArgument, hasItem("someElement")); -
}
Mocktio的局限
- 不能
mock静态方法; - 不能
mock构造器; - 不能
mockequals()和hashCode()方法。
如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一键三连哦!
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】


更多推荐


所有评论(0)