为软件编写测试是有益的,编写优秀的测试同样至关重要。
遵循以下原则:
- 保持测试代码简洁且可读:要毫不留情地进行重构,就像对生产代码所做的那样。否则,放任自流将导致测试端出现可怕的遗留代码。如果测试代码难以重构,那么生产代码也将难以重构,从而导致生产遗留代码的产生。始终遵循勇敢重构者的道路。
- 避免编写同义反复的代码:例如,测试代码使用与解析器中相同的正则表达式来生成内容。
- 一般来说,我们不希望在测试和代码之间重复逻辑:因此,在测试中复制正则表达式或其他内容不是一个选项。在这种情况下,思考测试输入刺激/输出结果(f(input) -> output)有所帮助,例如,如果代码旨在处理模板,则不要添加值,而是针对计算结果进行测试。
举例来看:
使用:
Assertions.assertThat(processTemplate("param1", "param2")).isEqualTo("this is 'param1', and this is 'param2'"));
而不使用:
Assertions.assertThat(processTemplate("param1", "param2")).isEqualTo(String.format("this is '%s', and this is '%s'", param1, param2));
-
尽可能覆盖广泛的范围,以展示正面案例,尤其是错误的代码路径。通常,在实践测试驱动开发(TDD)时最能实现这一点。通过TDD,可以在设计时识别出可能出错的地方。不要因为一个小问题而不好意思编写一个简单的测试用例。你永远不知道这段代码何时、为何或如何被使用甚至修改。
-
可以使用诸如PIT等工具进行变异测试,以检查测试的有效性。
-
不要模拟你不拥有的类型!这不是一条硬性规定,但跨越这条界限可能会产生后果!(很可能如此)
-
TDD既关乎设计也关乎测试。当模拟外部API时,测试无法用于驱动设计,因为API属于其他人;这个第三方可以并且会更改API的签名和行为
-
想象一下模拟第三方库的代码。在升级某个第三方库后,逻辑可能会稍作更改,但测试套件将正常运行,因为它是模拟的。因此,后来,当认为一切都准备好了,构建墙也绿了,软件部署后……却出现了问题。这可能是当前设计与第三方库的解耦程度不够的一个迹象。
-
另一个问题是,第三方库可能很复杂,甚至需要大量模拟才能正常工作。这会导致测试过于具体,且测试夹具复杂,从而本身就违背了简洁且可读的目标。或者由于模拟外部系统的复杂性,导致测试无法充分覆盖代码。
-
相反,最常见的方法是围绕外部库/系统创建包装器,但应注意抽象泄漏的风险,即过多的低级API、概念或异常会超出包装器的边界。为了验证与第三方库的集成,请编写集成测试,并使它们也尽可能简洁且可读。
-
其他人已经就模拟不拥有的类型的问题进行了撰写并经历了痛苦
-
不要模拟一切,这是一种反模式。如果一切都被模拟了,那我们真的在测试生产代码吗?不要犹豫,该不模拟就不模拟!
-
不要模拟值对象。为什么会有人想这么做呢?
-
因为实例化对象太痛苦了?!=> 这不是一个有效的理由。
-
如果创建新的夹具太困难,这表明代码可能需要一些严肃的重构。一种替代方法是为你的值对象创建构建器——有工具可以做到这一点,包括IDE插件、Lombok等。还可以在测试类路径中创建有意义的工厂方法。
-
final class CustomerCreations {
private CustomerCreations() {}
public static Customer customer_with_a_single_item_in_the_basket() {
// 初始化序列较长
}
}
Mockito关注的是对象之间的交互,这是面向对象编程(或消息传递)中最重要的部分。