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