每个Java开发者都会用JUnit。
有人连续几年在维护一个 Top 20 Java Libraries 排行榜,它使用了GitHub的API和Google BigQuery对超过27万(277975)个Java源代码文件分析而得出,2018年JUnit排名第三,此前JUnit连续三年蝉联冠军宝座。
分析的过程可以看我的另一篇文章 - 「小得103」2018年排名TOP100的Java库都有谁
几乎每个编程语言里都有JUnit的复制品,比如在.NET里NUnit,在C++里有CPPUnit。此外还有CUnit , PyUnit ,PHPUnit, OCUnit, DUnit, JSUnit 等等。
JUnit的诞生也足够传奇。
有一次坐飞机的时候Eric Gamma偶遇Kent Beck,两位大牛见面寒暄过以后就觉得很无聊了。旅途漫漫,干点啥好呢?
Kent Beck当时力推测试驱动开发(TDD), 但是没有一个工具或者框架能让大家轻松愉快的写测试,并且自动的运行测试。
两位大牛决定撸起袖子自己写一个,正好也可以实践一下XP里的结对编程 。等到飞机落地的时候,一个划时代的单元测试工具就新鲜出炉了,它的名字叫做JUnit。
Eric Gamma是设计模式四人帮(GoF)之一,后来还主导创建了Eclipse和Visual Studio Code。Kent Beck,是极限编程(XP)和测试驱动开发(TDD)的创建者。可以通过下面的图片领略一下两位大佬的风采。
多年前,我学习了Kent Beck的TDD后,十分依赖JUnit了。然而,当我第一次看到Spock的时,马上就移情别恋了。我喜欢Spock是因为:
不用再为测试的方法名命名而发愁了
我喜欢BDD的风格,尤其是生成的测试报告
表格化的数据驱动的测试太吸引人了
我喜欢Groovy,Spock是基于Groovy写的,Groovy所有的简洁魔幻的语法都可以使用;Spock的语法糖加上Groovy的语法糖,可以让测试代码减少一半儿,带来的简洁明了十分酸爽撩人
但是JUnit还是有Spock不及的优势的:
第一个就是“流行”,这意味着学习成本低,培训成本低,毕竟软件开发还是整个团队的事情,不是所有人愿意学习Spock的
也不是所有人愿意学习Groovy,但Java开发者都会Java,JUnit是基于Java的。另外,Java的强类型对IDE也特别友好,我一般实践TDD的时候,先用JUnit写测试代码,出错后敲击"option+回车",IntelliJ IDEA就会自动修复(比如生成还没有的类、方法等),过程十分流畅。而使用Spock后,IDE对Groovy这种动态语言的支持还是很不足的,这种行云流水的快感没有了
JUnit丰富的生态和兼容性是Spock所不能比的
TDD所推崇的先写测试代码,出现错误(编译错误或测试失败)IDE提示变红后,再写正式代码代码,让错误消失,IDE提示变绿。
在JUnit4的时代,我一有机会还是会选择Spock。但是,当JUnit5,这个下一代的JUnit版本正式发布后,也许可以重新选择JUnit了。
下面我们看看,JUnit5是怎么实现Spock的那些撩人的测试案例的。
方法命名
在Spock中可以这样命名测试方法:
def "events are published to all subscribers"() { }
现在JUnit5勉强可以应对了:
@Test @DisplayName("当充值420个积分后,账户中有520积分,并生成一条对应的充值记录") void recharge() { }
断言
Spock写断言太自然了,这要感谢Groovy:
pc.clockRate >= 2333 pc.os == "Linux" : "判断操作系统是Linux" //或者 with(pc) { vendor == "Sunny" clockRate >= 2333 ram >= 406 os == "Linux" }
输出的提示还十分友好:
assert pc.clockRate >= 2333 | | | | 1666 false ...
JUnit5得使用assertj来帮忙了:
assertThat(account.getBalance()).isEqualTo(new Amount(99)); assertThat(account.getConsumeRecords()) .hasSize(1) .hasOnlyOneElementSatisfying(record -> assertThat(record.getAmount()) .isEqualTo(new Amount(1)));
BDD风格
Spock最大亮点之一,Given-When-Than,不解释,代码一目了然:
def "HashMap accepts null key"() { given:"一个HashMap" def map = new HashMap() when:"给它添加null元素" map.put(null, "elem") then:"不抛出异常" notThrown(NullPointerException) }
可以实现“Specifications as Documentation”的效果,也可以生成非常好的报告。
JUnit5使用@Displayname和@Nested也可以勉强应对吧:
运行结果:
报告:
数据驱动测试
Spock的另一个大亮点,不解释看代码:
import spock.lang.Unroll class DataDrivenTest extends Specification { @Subject def arithmeticOperation = new ArithmeticOperation() @Unroll def "can add"() { expect: arithmeticOperation.add(1, b) >= 2 where: b << [1, 2, 3, 4, 5] } @Unroll def "can add #a to #b and receive #result"() { expect: arithmeticOperation.add(a, b) == result where: a | b | result 1 | 3 | 4 3 | 4 | 7 10 | 20 | 30 } }
执行效果:
JUnit5怎么实现呢?
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import java.util.stream.Stream; public class DataDrivenTest { private final ArithmeticOperation arithmeticOperation = new ArithmeticOperation(); @ParameterizedTest @ValueSource(ints = { 1, 2, 3, 4, 5 }) void canAdd(int b) { assertTrue(arithmeticOperation.add(1, b) >= 2); } @ParameterizedTest(name = "can add {0} to {1} and receive {2}") @MethodSource("additionProvider") void canAddAndAssertExactResult(int a, int b, int result) { assertEquals(result, arithmeticOperation.add(a, b)); } static Stream<Arguments> additionProvider() { return Stream.of( Arguments.of(1, 3, 4), Arguments.of(3, 4, 7), Arguments.of(10, 20, 30) ); } }
运行效果:
此外,还有Mock/Stub、按条件执行测试等功能,如果大家有兴趣看的话,后续再写。
总之,可以看到,JUnit5在表达性上,已经在努力追赶Spock了,但局限于Java语法的局限,还是有不小的差距的。但相对于JUnit4,JUnit5进步已经非常大了。如果你尤其是你的团队不想学习Spock/Groovy的话,或者喜欢IDE对Java更好的提示的话,那最起码可以考虑用JUnit5代替JUnit4了。