Mock让测试可控

测试不好测,关键是软件设计问题。一个好的设计可以把很多实现细节从业务代码中隔离出去。

之所以要隔离,一个重要原因就是实现细节不可控。

例如,我们依赖了数据库,就要保证整个数据库环境只有一个测试再用。理论上也可以,但是成本非常高。再比如,依赖了三方服务,我们没法控制它返回预期的值。这样一来,很多异常场景,我们都没法测试。

所以,在测试中,我们不能依赖于这些隔离出去的细节。

不依赖细节,但测试总要有一个有所需组件的实现吧。

没错,这就是Mock 框架。

Mock 框架

测试,本质是是在一个可控环境下对被测系统/组件进行试探。

拥有大量依赖三方代码,最大问题就是不可控。

如何变成可控呢?

  • 第一步自然是隔离
  • 第二步用可控组件代替不可控组件(用假组件代替真组件)

假组件有各种名词,例如 Stub、Fake、Spy、Mock等等。它们之间确实有差异,但差异几乎可以忽略不计。我们可以把假组件称为 Mock 对象。

Mock 框架的基本逻辑是,创建一个模拟对象并设置它行为。

当前 Java 社区最常用的 Mock 框架是 Mockito。

学习 Mock框架,必须掌握两个核心:

  • 设置模拟对象
  • 校验对象行为

设置 Mock 对象

创建一个 模拟对象,使用 框架的 mock 方法。

TodoItemRepository repository = mock(TodoItemRepository.class);

设置模拟对象的行为

when(repository.findAll()).thenReturn(of(new TodoItem("foo")));
when(repository.save(any())).then(returnsFirstArg());

好的程序库其API有很好的表达性,就像上面两端代码,即使不加说明,也大概知道会发生什么。

模拟对象的设置核心有两点:

  • 参数是什么
  • 对于处理是什么

参数匹配

参数设置是参数匹配的过程,就是判断给出的实参是否满足这里的条件。例如上面的代码,save 任意参数都可以,我们也可以设置特定的值。

when(repository.findByIndex(eq(1))).thenReturn(new TodoItem("foo"));

如果有更复杂的匹配过程,也可以自定义实现匹配过程。但是强烈建议不要这么做,因为测试应该是简单的。

一般来说,任意和相等大部分情况已经够用了。

模拟处理

我们来看如何设置相应的处理,只是模拟对象可控的关键。前面的例子我们看到了如何设置返回值,我们也可以抛出异常,模拟异常场景。


when(repository.save(any())).thenThrow(IllegalArgumentException.class);

同样的,处理也可以写的很复杂,但是强烈建议不要这么做,原因是,测试要简单.

校验模拟对象

校验模拟对象,就是知道一个方法有没有按照预期的方式调用。比如,我们可以预期 save 函数在执行过程中得到了调用。

verify(repository).save(any());

我们还可以校验整个方法调用了多少次

verify(repository, atLeast(3)).save(any());

校验有很多可以设置的参数,但是建议不要用的太复杂,连 verify 本身都不建议用太多。

一旦设置了 verify,实际上就约束了函数实现,就把函数的实现细节约定死了。一旦修改代码,verify 就很容易让测试无法通过。

**测试应该是测试的接口行为,而不是内部实现。**verify 虽好,但要少用。如果一些场景除了 verify 就没什么可断言的了,还是要用 verify。

总结

Mock框架 Mockito

  • 设置模拟对象
    • mock
  • 模拟对象参数
    • 参数匹配 any 和 eq
  • 模拟对象处理
    • thenReturn 和 thenThrow
  • 校验模拟对象行为
    • verify

使用 Mock 框架,少用 verify。

Last Updated:
Contributors: himcs