在我们日常的 TDD 开发中,永远绕不过去的就是要编写测试。而对于一个 Java 程序员,JUnit 似乎是一个不二的选择。它的确是一个十分优秀的工具,在大多数情况下都能够帮助我们完成测试的工作。
但是在开发过程中,我发现 JUnit 并不总是那么好用。它在一些情况下需要耗费挺多精力才能编写出让人满意的测试。
JUnit 不擅长的事情
一个让人满意的测试,应该能够清晰地体现被测试的目标、测试的目的以及测试的输入输出,并且应遵循 DRY 原则,尽可能的减少测试中的重复内容。
JUnit 可以通过设计测试方法名和组织方法内的代码的方式清晰地表达意图,也可以通过参数化测试来减少相同测试目的的代码重复。但是它在这些地方都做得不够好。
清晰表达测试的目的
在使用 JUnit 时,清晰的表达测试意图并不是总能做到的事情。这主要体现在两个方面。
如何命名测试方法
第一个体现就是在使用 Java 编写测试时,采用什么样的命名风格来命名测试。是为了代码风格的统一而选择驼峰?还是为了更高的可读性选择下划线?这个问题在不同的项目中有不同的实践,看起来是没有一个统一的认识。
而这个问题的根源是 JUnit 的测试名称是 Java 的方法名,而 Java 的方法名又不能在其中插入空格。所以除了下面要介绍的两种测试工具外,采用 Kotlin 来编写 JUnit 也是一种方式。
如何组织方法内的代码
第二个体现就是 JUnit 对测试方法内部如何编写没有强制的规定。这就意味着我可以在测试里面任意地组织代码,比如在一个测试方法里面对一个方法调用多次并验证每一次的结果,或者把调用测试目录的逻辑和准备数据的逻辑以及验证逻辑混合到一起。 总之这样的结果就是测试方法内的代码组织方式千奇百怪,每当阅读别人编写的测试的时候,总是要花上好几分钟才能知道这些代码到底在干什么。
对于这个问题,我个人会选择使用注释来标注一下 given
、 when
、 then
,并且给 IDEA 设置了 live template 方便插入它们。
又不是不能用的参数化测试
如果说不能清晰地表达测试意图这个问题还有一些 workaround 可以绕过去的话,JUnit 那仅仅是能用的参数化测试功能就没有什么好办法可以绕过去了。
JUnit 提供了各种 Source
注解来为参数化测试提供数据,但是这个功能实在是太弱了,很难让人满意。
难以让人满意的第一个原因是,各种 Source
注解基本上只能支持 7 种基本类型再加上 String
, Enum
和 Class
类型。如果想要使用其他类型的实例作为参数的话,就必须要使用 MethodSource
或者 ArgumentsSource
注解。
这就导致了第二个原因:这两个注解需要单独写一个静态方法或一个 ArgumentProvider
的实现,这就导致很难把测试参数写到测试代码旁边。并且 Arguments.of()
方法并不利于阅读测试参数。
这两点导致测试的可读性下降。而按照“测试即文档”的原则,我们应该尽力去保证测试的可读性。
第三个原因则是来自 ParameterizedTest
注解。它的 name
字段可以使用参数的索引值来把参数填入模板中,生成更加可读的测试名称。
但是它的功能也仅限于此了。因为这个模板只能使用索引值,不能使用索引后再调用里面的方法或者字段。所以如果我们的参数是一个复杂对象,那么一定要重写 toString
方法才能得到满意的输出。但是这又违背了编写测试的原则之一——不能为了测试而添加实现代码。
如果我们一定要得到一个更加表意的测试名称,那么添加一个专用的测试参数也能做到。但是这又会导致 IDE 或者构建工具的警告,因为它们认为这个参数没有被使用。
总之,尽管 JUnit 可以解决绝大多数问题,但是在这么几个小地方却做的不是那么完美。
那么有没有什么工具可以作为 JUnit 的替代呢?当然是有的。下面我将按照我接触的顺序来介绍两种种测试框架。可以在 GitHub 上找到下面例子的完整代码。
使用 Spock 作为测试框架
Spock是一个用 Groovy
编写的测试框架,按照 given/when/then
的结构定义 dsl,能够让测试更加的语义化。它的一大特点是 Data Driven Test,可以方便的编写参数化测试。
我曾在两个项目上尝试过使用 Spock
作为测试框架,几乎没有遇到过无法解决的问题。
如何使用 Spock
我们来看一个最简单的例子:
class MarsRoverSpockTest extends Specification {
// 1
def "should return mars rover position and direction when mars rover report"() {
//2
given: // 3.1
def marsRover = MarsRoverFixture.buildMarsRover(
position: new Position(1, 2),
direction: Direction.EAST,
)
when: // 3.2
def marsRoverInfo = marsRover.report()
then: // 3.3
marsRoverInfo.position == new Position(1, 2)
marsRoverInfo.direction == Direction.EAST
}
}
- 每一个测试都需要继承抽象类
Specification
- 可以使用字符串来命名测试
Spock
定义了一些 block,这里的given
、when
、then
都是 block。given
block 负责测试的 setup 工作when
block 可以是