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。