51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 2655|回复: 5
打印 上一主题 下一主题

[转贴] Java测试的自定义断言

[复制链接]
  • TA的每日心情
    无聊
    2024-9-19 09:07
  • 签到天数: 11 天

    连续签到: 2 天

    [LV.3]测试连长

    跳转到指定楼层
    1#
    发表于 2017-7-12 14:46:48 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    对于测试来说,编写断言似乎很简单:我们只需要对结果和预期进行比较,通常使用断言方法进行判断,例如测试框架提供的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个参数,已经太多了!)
    • 有时候,为了在多个测试中复用这些代码,会在这些方法中引入一些复杂逻辑(通常以布尔标志的形式校验它们,或在某些特殊的情况下,忽略它们)。

    从长远来看,所有使用私有断言方法编写的测试,意味着在可读性和可维护性方面将会遇到一些问题。我们来看一下另外一种没有这些缺点的解决方案。
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

  • TA的每日心情
    无聊
    2024-9-19 09:07
  • 签到天数: 11 天

    连续签到: 2 天

    [LV.3]测试连长

    2#
     楼主| 发表于 2017-7-12 14:49:20 | 只看该作者
    匹配器类库
    在我们继续之前,我们先来了解一些新工具。正如之前提到的,JUnit或者TestNG提供的断言缺少足够的灵活性。在Java世界,至少有两个开源类库能够满足我们的需求:AssertJ(FEST Fluent Assertions项目的一个分支)和?Hamcrest。我倾向于第一个,但这只是个人喜好。这两个看起来都非常强大,都能让你取得相似的效果。我更倾向于AssertJ的主要原因是它基于Fluent接口,而IDE能够完美支持该接口。
    集成AssertJ和JUnit或者TestNG非常简单。你只要增加所需的import,停止使用测试框架提供的默认断言方法,改用AssertJ提供的方法就可以了。
    AssertJ提供了一些现成的非常有用的断言。它们都使用相同的“模式”:先调用assertThat()方法,这是Assertions类的一个静态方法。该方法接收被测试对象作为参数,为更多的验证做好准备。之后是真正的断言方法,每一个都用于校验被测对象的各种属性。我们来看一些例子:
    [size=1em]CODE:
    [size=1em]1
    [size=1em]2
    [size=1em]3
    [size=1em]4
    [size=1em]5
    [size=1em]6
    [size=1em]7
    [size=1em]8
    [size=1em][size=1em]assertThat(myDouble).isLessThanOrEqualTo(2.0d);

    [size=1em]assertThat(myListOfStrings).contains("a");

    [size=1em]assertThat("some text")
    [size=1em]       .isNotEmpty()
    [size=1em]       .startsWith("some")
    [size=1em]       .hasLength(9);
    从这能看出,AssertJ提供了比JUnit和TestNG丰富得多的断言集合。就像最后一个assertThat(“some text”)例子显示的,你甚至可以将它们串在一起。还有一个非常方便的事情是,你的IDE能够根据被测对象的类型,自动为你提示可用的方法。举例来说,对于一个double值,当你输入“assertThat(myDouble).”,然后按下CTRL + SPACE(或者其它IDE提供的快捷键),IDE将为你显示可用的方法列表,例如isEqualTo(expectedDouble)、isNegative()或isGreaterThan(otherDouble),所有这些都可用于double值的校验。这的确是一个很酷的功能。
    自定义断言
    拥有AssertJ或者Hamcrest提供的更强大的断言集合的确很好,但对于HourRange类来说,这并不是我们真正想要的。匹配器类库的另一个功能是允许你编写自己的断言。这些自定义断言的行为将与AssertJ的默认断言一样,也就是说,你能够把它们串在一起。这正是我们接下来要做的。
    接下来我们将看到一个自定义断言的示例实现,但现在让我们先看看最终效果。这次我们将使用(我们自己的)RangeAssert类的assertThat()方法。
    [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][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]       List<Range> ranges = HourlyRange.getRanges(dateFrom, dateTo);
    [size=1em]       // then
    [size=1em]       RangeAssert.assertThat(ranges)
    [size=1em]            .hasSize(3)
    [size=1em]            .isSortedAscending()
    [size=1em]            .hasRange("2012-07-23 12:00", "2012-07-23 13:00")
    [size=1em]            .hasRange("2012-07-23 13:00", "2012-07-23 14:00")
    [size=1em]            .hasRange("2012-07-23 14:00", "2012-07-23 15:00");
    [size=1em]}
    即便是上面这么小的一个例子,我们也能看出自定义断言的一些优势。首先要注意的是//then后面的代码确实变少了,可读性也更好了。
    将自定义断言应用于更大的代码库时,将显现出其它优势。当我们继续使用自定义断言时,我们将注意到:
    • 可以很容易地复用它们。我们不强迫使用所有断言,但对特定测试用例,我们可以只选择那些重要的断言。
    • 特定领域语言属于我们,也就是说,对于特定测试场景,我们可以根据自己的喜好改变它(例如,传入Date对象,而不是字符串)。更重要的是这样的改变不会影响到其它测试。
    • 高可读性。毫无疑问,因为断言包括了很多小断言方法,每一个都只关注校验的很小的某个方面,因此可以为校验方法取一个恰当的名字。

    与私有断言方法相比,自定义断言的唯一不足是工作量要大一些。我们来看一下自定义断言的代码,它是否真的是一个很难的任务。
    要创建自定义断言,我们需要继承AssertJ的AbstractAssert类或者其子类。如下所示,我们的RangeAssert继承自AssertJ的ListAssert类。这很正常,因为我们的自定义断言将校验一个Range列表(List<Range>)。
    每一个使用AssertJ的自定义断言都会包含创建断言对象、注入被测对象的代码,然后可以使用更多的方法对其进行操作。如下面的代码所示,构造方法和静态assertThat()方法的参数都是List<Range>。
    [size=1em]CODE:
    [size=1em]1
    [size=1em]2
    [size=1em]3
    [size=1em]4
    [size=1em]5
    [size=1em]6
    [size=1em]7
    [size=1em]8
    [size=1em]9
    [size=1em][size=1em]public class RangeAssert extends ListAssert<Range> {

    [size=1em]  protected RangeAssert(List<Range> ranges) {
    [size=1em]    super(ranges);
    [size=1em]  }

    [size=1em]  public static RangeAssert assertThat(List<Range> ranges) {
    [size=1em]    return new RangeAssert(ranges);
    [size=1em]  }

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    无聊
    2024-9-19 09:07
  • 签到天数: 11 天

    连续签到: 2 天

    [LV.3]测试连长

    3#
     楼主| 发表于 2017-7-12 14:51:08 | 只看该作者
    现在我们看看RangeAssert类的其余内容。hasRange()和isSortedAscending()方法(显示在下一个代码列表中)是自定义断言方法的典型例子。它们具有以下共同点:
    • 它们都先调用isNotNull()方法,检查被测对象是否为null。确保这个校验不会失败并抛出NullPointerException异常消息。(这一步不是必须的,但建议有这一步)
    • 它们都返回“this”(也就是自定义断言类的对象,对应例子中RangeAssert类的对象)。这使得所有方法可以串在一起。
    • 它们都使用AssertJ Assertions类(属于AssertJ框架)提供的断言方法执行校验。
    • 它们都使用“真实”的对象(由父类ListAssert提供),确保Range列表(List<Range>)被校验。

    [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]19
    [size=1em]20
    [size=1em]21
    [size=1em]22
    [size=1em]23
    [size=1em]24
    [size=1em]25
    [size=1em]26
    [size=1em]27
    [size=1em]28
    [size=1em]29
    [size=1em]30
    [size=1em]31
    [size=1em]32
    [size=1em]33
    [size=1em][size=1em]private final static SimpleDateFormat SDF
    [size=1em]             = new SimpleDateFormat("yyyy-MM-dd HH:mm");

    [size=1em]     public RangeAssert isSortedAscending() {
    [size=1em]            isNotNull();
    [size=1em]            long start = 0;
    [size=1em]            for (int i = 0; i < actual.size(); i++) {
    [size=1em]                     Assertions.assertThat(start)
    [size=1em]                            .isLessThan(actual.get(i).getStart());
    [size=1em]                     start = actual.get(i).getStart();
    [size=1em]            }
    [size=1em]            return this;
    [size=1em]     }

    [size=1em]     public RangeAssert hasRange(String from, String to) throws ParseException {
    [size=1em]            isNotNull();

    [size=1em]            Long dateFrom = SDF.parse(from).getTime();
    [size=1em]            Long dateTo = SDF.parse(to).getTime();

    [size=1em]            boolean found = false;
    [size=1em]            for (Range range : actual) {
    [size=1em]                   if (range.getStart() == dateFrom && range.getEnd() == dateTo) {
    [size=1em]                           found = true;
    [size=1em]                   }
    [size=1em]            }
    [size=1em]            Assertions
    [size=1em]                   .assertThat(found)
    [size=1em]                   .isTrue();
    [size=1em]            return this;

    [size=1em]     }
    [size=1em]}
    那么错误信息呢?AssertJ让我们可以很容易地添加错误信息。对于简单的场景,例如值的比较,通常使用as()方法就足够了,示例如下:
    [size=1em]CODE:
    [size=1em]1
    [size=1em]2
    [size=1em]3
    [size=1em]4
    [size=1em][size=1em]Assertions
    [size=1em]            .assertThat(actual.size())
    [size=1em]            .as("number of ranges")
    [size=1em]            .isEqualTo(expectedSize);

    正如你所见到的,as()只是AssertJ框架提供的另一个方法。当测试失败时,它打印下面的信息,我们立即就能知道哪儿错了:
    [size=1em]CODE:
    [size=1em]1
    [size=1em]2
    [size=1em]3
    [size=1em][size=1em]org.junit.ComparisonFailure: [number of ranges]
    [size=1em]    Expected :4
    [size=1em]    Actual :3
    有时候只知道被测对象的名字是不够的,我们需要更多信息以了解到底发生了什么。以hasRange()方法为例,当测试失败时,如果能够打印所有range就更好了。我们可以通过overridingErrorMessage()方法来实现这种效果:
    [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][size=1em]public RangeAssert hasRange(String from, String to) throws ParseException {
    [size=1em]       ...
    [size=1em]       String errMsg = String.format("ranges\n%s\ndo not contain %s-%s",
    [size=1em]                                    actual ,from, to);

    [size=1em]       ...
    [size=1em]       Assertions.assertThat(found)
    [size=1em]              .overridingErrorMessage(errMsg)
    [size=1em]              .isTrue();
    [size=1em]       ...
    [size=1em]}

    现在,当测试失败时,我们能够得到非常详细的信息。它的内容取决于Range类的toString()方法。例如,它看起来可能是这样的:
    [size=1em]CODE:
    [size=1em]1
    [size=1em]2
    [size=1em]3
    [size=1em]4
    [size=1em][size=1em]HourlyRange{Mon Jul 23 12:00:00 CEST 2012 to Mon Jul 23 13:00:00 CEST 2012},
    [size=1em]HourlyRange{Mon Jul 23 13:00:00 CEST 2012 to Mon Jul 23 14:00:00 CEST 2012},
    [size=1em]HourlyRange{Mon Jul 23 14:00:00 CEST 2012 to Mon Jul 23 15:00:00 CEST 2012}]
    [size=1em]do not contain 2012-07-23 16:00-2012-07-23 14:00
    总结
    在本文中,我们讨论了很多编写断言的方法。我们从“传统”的方式开始,也就是基于测试框架提供的断言方法。对于很多场景,这已经非常好了。但是正如我们所看到的,它在表达测试意图时,有时候缺少了一些灵活性。之后,我们通过引入私有断言方法,取得了一点改善,但仍然不是理想的解决方案。最后,我们尝试使用AssertJ编写自定义断言,我们的测试代码取得了非常好的可读性和可维护性。
    如果要我提供一些关于断言的建议,我将会建议以下内容:如果你停止使用测试框架(例如JUnit或TestNG)提供的断言,改为使用匹配器类库(例如AssertJ或者Hamcrest),你的测试代码将得到极大的改善。你将可以使用大量可读性很强的断言,减少测试代码中//then之后的复杂声明。
    尽管编写自定义断言的成本非常低,但也没有必要因为你会写就一定要使用它们。当你的测试代码的可读性并且/或者可维护性变差时使用它们。根据我的经验,我会鼓励你在以下场景中使用自定义断言:
    • 当你发现使用匹配器类库提供的断言无法清晰表达测试意图时;
    • 作为私有断言方法的替代方案。

    我的经验告诉我,单元测试几乎不需要自定义断言。而在集成测试和端到端测试(功能测试)中,我敢说你肯定会发现它们是不可替代的。它们能让你的测试用领域语言说话(而不是实现语言),它们还封装了技术细节,使测试更易于更新。
    关于作者
    Tomek Kaczanowski是CodeWise公司(克拉科夫,波兰)的一名Java开发人员。他专注于代码质量、测试和自动化。他是TDD的狂热者、开源的倡导者和敏捷的崇拜者。具有强烈的分享知识倾向。书的作者、博客和会议发言人。Twitter: @tkaczanowski

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    回复 支持 反对

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-18 03:02 , Processed in 0.074001 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表