对于测试来说,编写断言似乎很简单:我们只需要对结果和预期进行比较,通常使用断言方法进行判断,例如测试框架提供的assertTrue()或者assertEquals()方法。然而,对于更复杂的测试场景,使用这些基础的断言验证结果可能会显得相当笨拙。 使用这些基础断言的主要问题是,底层细节掩盖了测试本身,这是我们不希望看到的。在我看来,应该争取让这些测试使用业务语言来说话。 在本篇文章中,我将展示如何使用“匹配器类库”(matcher library);来实现自定义断言,从而提高测试代码的可读性和可维护性。 ?
为了方便演示,我们假设有这样一个任务:让我们想象一下,我们需要为应用系统的报表模块开发一个类,输入两个日期(开始日期和结束日期),这个类将给出这两个日期之间所有的每小时间隔。然后使用这些间隔从数据库查询所需数据,并以直观的图表方式展现给最终用户。 标准方法我们先采用“标准”的方法来编写断言。我们以JUnit为例,当然你也可以使用TestNG。我们将使用像assertTrue()、assertNotNull()或assertSame()这样的断言方法。 下面展示了HourRangeTest类的其中一个测试方法。它非常简单。首先调用getRanges()方法,得到两个日期之间所有的每小时范围。然后验证返回的范围是否正确。 [size=1em]CODE:[size=1em]01 [size=1em]02 [size=1em]03 [size=1em]04 [size=1em]05 [size=1em]06 [size=1em]07 [size=1em]08 [size=1em]09 [size=1em]10 [size=1em]11 [size=1em]12 [size=1em]13 [size=1em]14 [size=1em]15 [size=1em]16 [size=1em]17 [size=1em]18 | [size=1em][size=1em]private final static SimpleDateFormat SDF
[size=1em] = new SimpleDateFormat("yyyy-MM-dd HH:mm");
[size=1em]@Test
[size=1em]public void shouldReturnHourlyRanges() throws ParseException {
[size=1em] // given
[size=1em] Date dateFrom = SDF.parse("2012-07-23 12:00");
[size=1em] Date dateTo = SDF.parse("2012-07-23 15:00");
[size=1em] // when
[size=1em] final List<range> ranges = HourlyRange.getRanges(dateFrom, dateTo);
[size=1em] // then
[size=1em] assertEquals(3, ranges.size());
[size=1em] assertEquals(SDF.parse("2012-07-23 12:00").getTime(), ranges.get(0).getStart());
[size=1em] assertEquals(SDF.parse("2012-07-23 13:00").getTime(), ranges.get(0).getEnd());
[size=1em] assertEquals(SDF.parse("2012-07-23 13:00").getTime(), ranges.get(1).getStart());
[size=1em] assertEquals(SDF.parse("2012-07-23 14:00").getTime(), ranges.get(1).getEnd());
[size=1em] assertEquals(SDF.parse("2012-07-23 14:00").getTime(), ranges.get(2).getStart());
[size=1em] assertEquals(SDF.parse("2012-07-23 15:00").getTime(), ranges.get(2).getEnd());
[size=1em]}
|
毫无疑问这是个有效的测试。然而,它有个严重的缺点。在//then后面有大量的重复代码。显然,它们是复制和粘贴的代码,经验告诉我,它们将不可避免地会产生错误。此外,如果我们写更多类似的测试(我们肯定还要写更多的测试来验证HourlyRange类),同样的断言声明将在每一个测试中不断地重复。 过多的断言和每个断言的复杂性减弱了当前测试的可读性。大量的底层噪音使我们无法快速准确地了解这些测试的核心场景。我们都知道,阅读代码的次数远大于编写的次数(我认为这同样适用于测试代码),所以我们理所当然地要想办法提高其可读性。 在我们重写这些测试之前,我还想重点说一下它的另一个缺点,这与错误信息有关。例如,如果getRanges()方法返回的其中一个Range与预期不同,我们将得到类似这样的信息: [size=1em]CODE:[size=1em]1 [size=1em]2 [size=1em]3 | [size=1em][size=1em]org.junit.ComparisonFailure:
[size=1em]Expected :1343044800000
[size=1em]Actual :1343041200000
|
这些信息太不清晰,理应得到改善。 私有方法那么,我们究竟能做些什么呢?好吧,最显而易见的办法是将断言抽成一个私有方法: [size=1em]CODE:[size=1em]01 [size=1em]02 [size=1em]03 [size=1em]04 [size=1em]05 [size=1em]06 [size=1em]07 [size=1em]08 [size=1em]09 [size=1em]10 [size=1em]11 [size=1em]12 [size=1em]13 [size=1em]14 [size=1em]15 [size=1em]16 [size=1em]17 [size=1em]18 | [size=1em][size=1em]private void assertThatRangeExists(List<Range> ranges, int rangeNb,
[size=1em] String start, String stop) throws ParseException {
[size=1em] assertEquals(ranges.get(rangeNb).getStart(), SDF.parse(start).getTime());
[size=1em] assertEquals(ranges.get(rangeNb).getEnd(), SDF.parse(stop).getTime());
[size=1em]}
[size=1em]@Test
[size=1em]public void shouldReturnHourlyRanges() throws ParseException {
[size=1em] // given
[size=1em] Date dateFrom = SDF.parse("2012-07-23 12:00");
[size=1em] Date dateTo = SDF.parse("2012-07-23 15:00");
[size=1em] // when
[size=1em] final List<Range> ranges = HourlyRange.getRanges(dateFrom, dateTo);
[size=1em] // then
[size=1em] assertEquals(ranges.size(), 3);
[size=1em] assertThatRangeExists(ranges, 0, "2012-07-23 12:00", "2012-07-23 13:00");
[size=1em] assertThatRangeExists(ranges, 1, "2012-07-23 13:00", "2012-07-23 14:00");
[size=1em] assertThatRangeExists(ranges, 2, "2012-07-23 14:00", "2012-07-23 15:00");
[size=1em]}
|
这样是不是好些?我会说是的。减少了重复代码的数量,提高了可读性,这当然是件好事。 这种方法的另一个优势是,我们现在可以更容易地改善验证失败时的错误信息。因为断言代码被抽到了一个方法中,所以我们可以改善断言,很容易地提供更可读的错误信息。 为了更好地复用这些断言方法,可以将它们放到测试类的基类中。 不过,我觉得我们也许能做得更好:使用私有方法也有缺点,随着测试代码的增长,很多测试方法都将使用这些私有方法,其缺点将更加明显: - 断言方法的命名很难清晰反映其校验的内容。
- 随着需求的增长,这些方法将会趋向于接收更多的参数,以满足更复杂检查的要求。(assertThatRangeExists()现在有4个参数,已经太多了!)
- 有时候,为了在多个测试中复用这些代码,会在这些方法中引入一些复杂逻辑(通常以布尔标志的形式校验它们,或在某些特殊的情况下,忽略它们)。
从长远来看,所有使用私有断言方法编写的测试,意味着在可读性和可维护性方面将会遇到一些问题。我们来看一下另外一种没有这些缺点的解决方案。 |