作为测试驱动设计和开发的忠实粉丝, 我相信创造良好的测试是我们作为 Java 开发人员可以做的最重要的事情之一. 我们写测试出于许多原因:
塑造系统的设计. 我们知道输入和输出应该是什么样的, 但是我们需要创建什么对象来做到这一点呢? 代码应该塑造成什么样的 "形状"? 编写测试可以让我们知道应该创建什么样的代码.
为了确保初始和持续的正确性. 让我们的应用程序如期望地那样运作并且始终如一地精确很重要. 测试应该竭力确保做到这一点.
文档. 测试是系统的文档, 因为它会说明它应该做什么以及应该怎么做.
那么 "好的测试" 到底是什么样子的呢?
给测试命名
测试的名字至关重要, 特别是从文档角度来看的话. 我们应该能够大声读出测试的名字就像一组需求一样. 事实上, 有一个伟大的 IntelliJ 插件, 叫 Enso, 它会将你的测试名转变为恰好位于每个类旁边的语句, 这样你就可以明明白白地看到你在做什么.
不要以 "test" 开始命名测试的名称. 这是来自于 JUnit 初期的后遗症, 当需要它执行的时候. 你的 Test 类将在 Test 文件夹中, 在一个最后有 Test 这个单词的类中. 会有一个 @Test 的注解. 我们知道这是一个测试.
你也应该避免以 "should" 或 "will" 开头. 这些都是干扰词. 既然你已经为这个功能写了一个测试, 那我们就知道它 "should 或 will" 工作 (如果不能工作的话, 那我们知道我们需要修复它).
将测试名称当作一个要求. 下面是一些例子
- addingNumbersWillSumValuesTogether()
- explodesOnNegativeID()
- notifiesListenersOnUpdates()
不要害怕表达出来. 如果你的测试名称确实需要很长的一串单词, 那就这么做, 只要它能清楚说明将发生什么事情.
测试代码
测试将分为 3 个部分: 设置, 操作, 断言.
设置
对你的测试设置代码应该只与在测试中被断言的值相关. 如果你有多余的设置代码, 那就会搞不清楚它是什么, 并且与测试不相关.
这可以通过多种方式实现:
将通用设置移动到使用 @Before 注解的具体设置方法.
将重复的设置代码移动到辅助方法
使用 Maker 来创建复杂的测试对象, 并只设置测试中相关的值.
我重申一下: 每个测试的设置部分应该只有与最后被断言的值相关的代码.
不好的例子:
- @Test
- public void returnsBooksWherePartialTitleMatchesInAnyCast(){
- Bookstore bookstore = new Bookstore();
- Book harryPotterOne = new Book("Harry Potter and The Philosopher Stone");
- bookstore.add(harryPotterOne);
- bookstore.add(new Book("Guardians of the Galaxy"));
- Book harryPotterTwo = new Book("The Truth about HARRY POTTER");
- bookstore.add(harryPotterTwo);
- List<Book> results = bookstore.findByTitle("RY pot");
- assertThat(results.size(), is(2));
- assertThat(results, containsInAnyOrder(harryPotterOne, harryPotterTwo));
- }
书店的初始化发生在测试中, 书本的创建也是. 这让测试显得混乱不堪, 让人搞不清楚发生了什么事情.
好的例子:
- private Bookstore bookstore = new Bookstore();
- private Book aHarryPotterBook = new Book("Harry Potter and The Philosopher Stone");
- private Book anotherHarryPotterBook = new Book("The Truth about HARRY POTTER");
- private Book aBook = new Book("Guardians of the Galaxy");
- @Test
- public void returnsBooksWherePartialTitleMatchesInAnyCast(){
- bookstore.add(aHarryPotterBook);
- bookstore.add(aBook);
- bookstore.add(anotherHarryPotterBook);
- List<Book> results = bookstore.findByTitle("RY pot");
- assertThat(results.size(), is(2));
- assertThat(results, containsInAnyOrder(aHarryPotterBook, anotherHarryPotterBook));
- }
初始化发生在字段中, 这样在测试中发生了什么一清二楚.
操作
小菜一碟! 最好保持到一行, 你要进行测试的独立操作. 有时候, 你专门测试的是输出是什么, 如果某些东西被多次调用, 或者在某些优先操作之后调用的结果是什么, 所以这不是一个硬性规定. 当读取测试时, 用户应该快速而轻松地能说 "将这些值设置成这样, 如果我执行这个操作 / 这些操作, 那么这是预期的结果". 在上面的例子中, 便是 bookstore.findByTitle() 方法.
断言
使用 Hamcrest. Hamcrest 是一个很棒的库, 给我们一个流畅的 API 用来写入测试. 不会像这样的代码:
- assertEquals(results.size(), 2);
- assertTrue(results.contains(aHarryPotterBook))
- assertTrue(results.contains(anotherHarryPotterBook))
我们可以一目了然, 轻松地阅读像这样的代码:
- assertThat(results.size(), is(2));
- assertThat(results, containsInAnyOrder(aHarryPotterBook, anotherHarryPotterBook));
这些相当简单的例子: Hamcrest 有很多伟大的方法, 使编写复杂测试变得很容易, 并允许你创建自己的匹配器.
当然, 理想情况下, 我们希望有一个独立的断言. 这可以让我们知道我们正在测试什么, 并说明我们的代码没有意外情况. 就像这篇文章中所说的那样, 这不是一个硬性的规则, 因为在某些情况下, 这是必要的, 但如果你有这样一个的测试:
- assertThat(orderBook.bids.size(), is(4));
- assertThat(orderBook.asks.size(), is(3));
- assertThat(orderBook.bids.get(0).price, is(5200));
- assertThat(orderBook.asks.get(2).price, is(10000000));
- assertThat(orderBook.asks.get(2).isBuy, is(false));
那么要理解测试哪里失败或哪条断言重要就变得困难多了.
你也可以在 Hamcrest 中编写自定义的匹配器, 因为 Hamcrest 可为复杂断言提供一个优雅的解决方案. 如果你需要在一个循环中运行断言, 或者你有大量的字段要断言, 那么一个自定义的匹配器可能才是上上之选.
一个测试的最重要的部分之一是, 当它失败时, 哪怕是一个 5 岁孩子也应该看得出什么地方出了错以及哪里错了. 失败的消息一定不能含糊. 关于这方面的解决方法是:
如果做任何类型的对象比较, 那么保证对象有一个体面的 toString() 消息. 没有什么比 < MyObject @ 142131 > 不匹配更糟的了.
想要做的更好的话, 可以对你的对象使用自定义匹配器. 你可以准确地知道哪些字段未能匹配.
确保明确为什么你要选择和这个值作比较. 例如, 如果你正在将一个字段值与数字 3000 比较, 那么为什么是 3000? 你应该费力地明白这一点. 显然, 这个数字不是随便得来的, 并且还要确保该变量的命名可以显示它的值是如何得来的.
所有这些都应该是在一个适度的常识范围内. 没有严格规定!
你还有什么要补充的吗? 欢迎告诉我们.
译文链接: http://www.codeceo.com/good-java-test.html
来源: http://www.codeceo.com/good-java-test.html