98

关于Arrange-Act-Assert的经典测试模式,我经常发现自己在 Act 之前添加了一个反断言。这样我就知道传递的断言确实是作为动作的结果传递的。

我认为它类似于 red-green-refactor 中的红色,只有当我在测试过程中看到红色条时,我才知道绿色条意味着我编写的代码有所作为。如果我写了一个通过测试,那么任何代码都会满足它;同样,关于 Arrange-Assert-Act-Assert,如果我的第一个断言失败,我知道任何 Act 都会通过最终 Assert - 因此它实际上并没有验证关于该 Act 的任何内容。

您的测试是否遵循这种模式?为什么或者为什么不?

更新说明:初始断言本质上与最终断言相反。这不是断言 Arrange 有效。这是一个断言Act还没有奏效。

4

14 回答 14

129

这不是最常见的事情,但仍然很常见,可以拥有自己的名字。这种技术称为Guard Assertion您可以在 Gerard Meszaros 的优秀书籍xUnit Test Patterns(强烈推荐)的第 490 页找到它的详细描述。

通常,我自己不使用这种模式,因为我发现编写一个特定的测试来验证我认为需要确保的任何先决条件更正确。如果前提条件失败,这样的测试应该总是失败,这意味着我不需要将它嵌入到所有其他测试中。这提供了更好的关注点隔离,因为一个测试用例只验证一件事。

对于给定的测试用例,可能需要满足许多先决条件,因此您可能需要多个 Guard Assertion。不是在所有测试中重复这些,而是​​对每个前提条件进行一个(并且只有一个)测试,这样可以使您的测试代码更具可维护性,因为这样您将减少重复。

于 2009-06-20T05:39:04.827 回答
32

它也可以指定为 Arrange - Assume -Act-Assert。

在 NUnit 中有一个技术处理,如这里的示例:http: //nunit.org/index.php ?p=theory&r=2.5.7

于 2010-08-19T18:59:04.650 回答
8

这是一个例子。

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
    range.encompass(7);
    assertTrue(range.includes(7));
}

可能是我写信Range.includes()只是为了返回 true。我没有,但我可以想象我可能有。或者我可能以任何其他方式写错了。我希望并期望使用 TDD 时我实际上做对了——这includes()很有效——但也许我没有。所以第一个断言是一个健全性检查,以确保第二个断言是真正有意义的。

单独阅读,assertTrue(range.includes(7));就是说:“断言修改后的范围包括 7”。在第一个断言的上下文中阅读,它是说:“断言调用包含()会导致它包含 7。由于包含是我们正在测试的单元,我认为这是一些(小)价值。

我接受我自己的回答;许多其他人误解了我的问题是关于测试设置。我认为这略有不同。

于 2009-06-23T18:59:14.287 回答
7

一个Arrange-Assert-Act-Assert测试总是可以重构为两个测试:

1. Arrange-Assert

2. Arrange-Act-Assert

第一个测试只会断言在安排阶段设置的内容,第二个测试只会断言发生在动作阶段的内容。

这样做的好处是可以更准确地反馈是安排阶段还是行动阶段失败,而在原始版本中,Arrange-Assert-Act-Assert这些是混为一谈的,您必须深入挖掘并准确检查哪些断言失败以及失败的原因才能知道是否失败的是安排或法案。

它还更好地满足了单元测试的意图,因为您将测试分成更小的独立单元。

于 2014-02-28T15:43:42.190 回答
5

我现在正在这样做。不一样的AAAA

Arrange - setup
Act - what is being tested
Assemble - what is optionally needed to perform the assert
Assert - the actual assertions

更新测试示例:

Arrange: 
    New object as NewObject
    Set properties of NewObject
    Save the NewObject
    Read the object as ReadObject

Act: 
    Change the ReadObject
    Save the ReadObject

Assemble: 
    Read the object as ReadUpdated

Assert: 
    Compare ReadUpdated with ReadObject properties

原因是 ACT 不包含 ReadUpdated 的读取是因为它不是该行为的一部分。行为只是改变和拯救。所以真的,ARRANGE ReadUpdated 用于断言,我正在调用 ASSEMBLE 进行断言。这是为了防止混淆 ARRANGE 部分

ASSERT 应该只包含断言。这样就在 ACT 和设置断言的 ASSERT 之间留下了 ASSEMBLE。

最后,如果你在安排中失败了,你的测试是不正确的,因为你应该有其他测试来防止/发现这些微不足道的错误。因为对于我目前的场景,应该已经有其他测试 READ 和 CREATE 的测试。如果你创建一个“Guard Assertion”,你可能会破坏 DRY 并创建维护。

于 2011-09-21T04:31:50.917 回答
2

我不使用那种模式,因为我认为做类似的事情:

Arrange
Assert-Not
Act
Assert

可能毫无意义,因为假设您知道您的 Arrange 部分工作正常,这意味着 Arrange 部分中的任何内容都必须进行测试,或者足够简单以至于不需要测试。

使用您的答案示例:

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7)); // <-- Pointless and against DRY if there 
                                    // are unit tests for Range(int, int)
    range.encompass(7);
    assertTrue(range.includes(7));
}
于 2012-08-26T05:15:58.700 回答
1

在执行您正在测试的操作之前,加入“健全性检查”断言来验证状态是一种古老的技术。我通常将它们写为测试脚手架,以向自己证明测试符合我的预期,然后将它们删除以避免测试脚手架使测试变得混乱。有时,留下脚手架有助于测试作为叙述。

于 2009-06-20T05:23:23.490 回答
1

我已经读过这种技术——可能来自你的btw——但我不使用它;主要是因为我习惯了用于单元测试的 AAA 格式。

现在,我开始好奇,并且有一些问题:您如何编写测试,是否导致此断言失败,遵循红-绿-红-绿-重构循环,还是之后添加?

你是否有时会失败,也许是在你重构代码之后?这告诉你什么?也许你可以分享一个有帮助的例子。谢谢。

于 2009-06-20T17:31:03.157 回答
1

我之前在调查失败的测试时已经这样做了。

在相当头疼之后,我确定原因是“安排”期间调用的方法没有正常工作。测试失败具有误导性。我在安排后添加了一个断言。这使得测试在突出实际问题的地方失败。

如果测试的 Arrange 部分太长太复杂,我认为这里也会有代码味道。

于 2010-08-05T10:57:35.910 回答
1

总的来说,我非常喜欢“Arrange, Act, Assert”,并将其作为我的个人标准。然而,它没有提醒我做的一件事是,当断言完成时,我已经安排好了。在大多数情况下,这不会引起太大的烦恼,因为大多数事情会通过垃圾收集等自动消失。但是,如果您已经建立了与外部资源的连接,您可能希望在完成后关闭这些连接根据您的断言,或者您的许多人在某处拥有服务器或昂贵的资源,这些资源可以保持连接或重要资源,它应该能够赠送给其他人。如果您是不使用 TearDown 或 TestFixtureTearDown 的开发人员之一,这一点尤其重要在一项或多项测试后进行清理。当然,“Arrange, Act, Assert” 不对我没有关闭我打开的内容负责;我只提到这个“gotcha”是因为我还没有找到一个好的“dispose”的“A-word”同义词来推荐!有什么建议么?

于 2011-08-03T23:20:06.573 回答
1

查看 Wikipedia 关于按合同设计的条目。Arrange-Act-Assert 三位一体是对一些相同概念进行编码的尝试,是关于证明程序正确性的。来自文章:

The notion of a contract extends down to the method/procedure level; the
contract for each method will normally contain the following pieces of
information:

    Acceptable and unacceptable input values or types, and their meanings
    Return values or types, and their meanings
    Error and exception condition values or types that can occur, and their meanings
    Side effects
    Preconditions
    Postconditions
    Invariants
    (more rarely) Performance guarantees, e.g. for time or space used

在设置它所花费的精力和它增加的价值之间需要权衡。AAA 是对所需最少步骤的有用提醒,但不应阻止任何人创建额外步骤。

于 2013-11-01T00:05:00.823 回答
0

取决于您的测试环境/语言,但通常如果 Arrange 部分中的某些内容失败,则会引发异常并且测试失败并显示它而不是启动 Act 部分。所以不,我通常不使用第二个 Assert 部分。

此外,如果您的 Arrange 部分非常复杂并且并不总是抛出异常,您可能会考虑将其包装在某个方法中并为其编写自己的测试,这样您就可以确保它不会失败(没有抛出异常)。

于 2009-06-20T05:22:07.623 回答
0

如果您真的想测试示例中的所有内容,请尝试更多测试...例如:

public void testIncludes7() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
}

public void testIncludes5() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(5));
}

public void testIncludes0() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(0));
}

public void testEncompassInc7() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(7));
}

public void testEncompassInc5() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(5));
}

public void testEncompassInc0() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(0));
}

因为否则你会错过很多错误的可能性......例如,在包含之后,范围仅包括 7 等......还有范围长度的测试(以确保它不包含随机值),并且另一组完全试图在范围内包含 5 的测试......我们会期望什么 - 包含异常,或者范围保持不变?

无论如何,关键是如果您想要测试的行为中有任何假设,请将它们放在自己的测试中,是吗?

于 2015-08-26T00:51:19.573 回答
0

我用:

1. Setup
2. Act
3. Assert 
4. Teardown

因为干净的设置非常重要。

于 2017-04-10T11:55:23.153 回答