它应该是“安排—声明—行为—声明”吗?


94

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

我认为它类似于红色-绿色-重构中的红色,在红色-绿色-重构中,只有在测试过程中看到红色条时,我才知道绿色条表示我已经编写了有区别的代码。如果我编写了通过测试,那么任何代码都可以满足要求;类似地,对于“安排—断言—行为—断言”,如果我的第一个断言失败,我知道任何法案都会通过最终的断言,因此实际上并没有验证有关该法案的任何内容。

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

更新说明:初始声明与最终声明实质上相反。这不是Arrange工作的断言;有人断言该法案尚未生效。

Answers:


121

这不是最常见的做法,但仍然足够常见以拥有自己的名称。此技术称为“ 警卫断言”。您可以在第490页的Gerard Meszaros 撰写的出色的xUnit Test Patterns(强烈推荐)一书中找到它的详细说明。

通常,我自己不会使用此模式,因为我发现编写特定的测试来验证我认为需要确保的任何前提条件更为正确。如果前提条件失败,则此类测试应始终失败,这意味着我不需要将其嵌入所有其他测试中。由于一个测试用例只能验证一件事,因此可以更好地隔离问题。

对于给定的测试用例,可能需要满足许多先决条件,因此您可能需要多个Guard Assertion。不必在所有测试中都重复这些测试,而是对每个前提条件进行一个(仅一个)测试可以使您的测试代码更易于维护,因为这样您的重复次数会更少。


+1,很好的答案。最后一部分特别重要,因为它表明您可以作为单独的单元测试来保护事物。
murrekatt 2011年

3
我通常也这样做,但是使用单独的测试来确保前提条件(尤其是具有变化需求的大型代码库)存在一个问题-前提条件测试会随着时间的流逝而被修改并与“主”条件不同步以那些前提为前提的测试。因此,前提条件可能全是绿色的,但是主测试中不满足这些前提条件,该主测试现在始终显示绿色和良好的条件。但是,如果先决条件在主要测试中,则它们将失败。您是否遇到了这个问题,并找到了一个不错的解决方案?
nchaud 2014年

2
如果您对测试进行大量更改,则可能会遇到其他问题,因为这会使您的测试不那么可信。即使面对不断变化的需求,也可以考虑以仅追加方式设计代码
马克·塞曼

@MarkSeemann是的,我们必须尽量减少重复,但是另一方面,可能会有很多事情会影响特定测试的Arrange,尽管对Arrange本身的测试会通过。例如,对Arrange测试或其他测试的清除错误,并且Arrange与Arrange测试中的不同。
Rekshino


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”。在第一个断言的上下文中读到,它的意思是:“断言调用contain()会使它包含7。由于contains是我们正在测试的单元,因此我认为它具有一些(较小)价值。

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


感谢您举一个例子,卡尔。好吧,在TDD周期的红色部分,直到contains()确实起作用为止。第一个断言毫无意义,只是第二个断言的重复。从绿色开始,它开始变得有用。在重构过程中,这变得很有道理。具有自动执行此操作的UT框架可能会很好。
philant

假设您TDD了Range类,当您破坏Range ctor时,会不会再有一次失败的测试来测试Range ctor?
09年

1
@philippe:我不确定我是否理解这个问题。Range构造函数和include()有自己的单元测试。您能详细说明一下吗?
卡尔·马纳斯特

对于第一个assertFalse(range.includes(7))失败的声明,您需要在Range构造函数中存在一个缺陷。所以我想问一下Range构造函数的测试是否不会在该断言的同时中断。那么,在Act之后对另一个值进行断言如何呢?例如assertFalse(range.includes(6))呢?
philant

1
在我看来,范围构建先于include()之类的函数。因此,虽然我同意,但只有错误的构造函数(或错误的include())会导致第一个断言失败,而构造函数的测试不会包含对include()的调用。是的,直到第一个断言为止的所有功能都已经过测试。但是这个最初的负面主张正在传达一些东西,在我看来,是一些有用的东西。即使每个这样的声明在最初被写入时都通过了。
Carl Manaster 09年

7

一个Arrange-Assert-Act-Assert测试始终可以重构为两个测试:

1. Arrange-Assert

2. Arrange-Act-Assert

第一个测试仅声明在“安排”阶段中设置的内容,第二个测试仅声明在“ Act”阶段中发生的内容。

这样做的好处是,对于原始阶段的“安排”或“法案”阶段失败,可以提供更精确的反馈 Arrange-Assert-Act-Assert它们是混杂的,因此您必须更深入地研究并确切地检查断言是什么以及为什么断言才能知道是否是安排或法案失败了。

由于将测试分为较小的独立单元,因此它也更好地满足了单元测试的意图。

最后,请记住,每当您在不同的测试中看到类似的“安排”部分时,都应尝试将其引入共享的帮助器方法中,以使您的测试将来更干燥,更易于维护。


3

我现在正在这样做。另一种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,从而建立了断言。

最后,如果您未通过Arrange,则您的测试不正确,因为您应该进行其他测试来防止/发现这些琐碎的错误。因为对于我目前存在的场景,应该已经有其他测试来测试READ和CREATE。如果创建“保护声明”,则可能会破坏DRY并创建维护。


1

在执行要测试的动作之前,先通过“健全性检查”断言来验证状态是一种古老的技术。我通常将它们写为测试脚手架,以向自己证明该测试能够达到我的期望,并在以后删除它们以避免测试脚手架使测试混乱。有时,将脚手架留在里面有助于测试作为叙述。


1

我已经读过有关此技术的信息-也许是从您那里得到的-但我不使用它;主要是因为我习惯了单元测试的Triple A形式。

现在,我很好奇,并提出了一些问题:如何编写测试,在执行红绿红绿重构周期之后,是否使该断言失败,还是在事后添加它?

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


我通常不强迫初始断言失败-毕竟,在编写其方法之前,它不应以TDD断言的方式失败。我确实会在编写测试时编写它,之前,只是在编写测​​试的正常过程中,而不是之后。老实说,我不记得它失败了-也许这表明那是浪费时间。我将尝试举一个例子,但是目前我还没有想到。谢谢你的提问;他们很有帮助。
Carl Manaster 09年

1

在调查失败的测试之前,我已经做到了。

经过相当大的挠头后,我确定原因是“整理”期间调用的方法无法正常工作。测试失败具有误导性。我在安排之后添加了断言。这使得测试无法通过突出实际问题的地方进行。

我认为,如果测试的“安排”部分过长且过于复杂,这里也会有代码异味。


一点要点:我认为过于复杂的Arrange比代码气味更像是一种设计气味-有时设计如此,只有复杂的Arrange才能让您测试单元。我之所以提到它,是因为这种情况需要的解决方案比简单的代码味道更深。
卡尔·马纳斯特

1

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


1
@carlmanaster,您实际上对我来说足够亲密!我将其粘贴在我的下一个TestFixture中,以尝试进行尺寸调整。就像那条小提醒提醒您做母亲应该教给您的事情:“如果您将其打开,请将其关闭!如果您弄乱了,请清理干净!” 也许其他人可以改进它,但是至少它以“ a!”开头。感谢您的建议!
约翰·托伯勒

1
@carlmanaster,我确实尝试过“ Annul”。它比“拆解”更好,并且可以工作,但是我仍在寻找另一个像“安排,行动,断言”一样完美地贴在我头上的“ A”字。也许是“歼灭?!”
John Tobler

1
所以现在,我有了“安排,承担,行动,断言,歼灭”。嗯!我太复杂了,是吗?也许我最好还是别吻,然后回到“安排,表演和断言!”。
约翰·托伯勒

1
也许使用R进行重置?我知道它不是A,但听起来像是海盗在说:Aaargh!并使用Assert重置押韵:o
Marcel Valdez Orozco,2012年

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是一个有用的提醒,它提示了所需的最少步骤,但不应阻止任何人创建其他步骤。


0

取决于您的测试环境/语言,但是通常如果“编配”部分中的某些操作失败,则会引发异常,并且测试无法显示该异常,而不是启动“ Act”部分。所以不,我通常不使用第二个断言部分。

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


0

我不使用该模式,因为我认为这样做:

Arrange
Assert-Not
Act
Assert

可能没有意义,因为据说您知道您的“编配”部分正常工作,这意味着“编排”部分中的所有内容也必须进行测试,或者必须足够简单才能进行测试。

使用您的答案示例:

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));
}

恐怕您不太了解我的问题。最初的断言不是关于测试Arrange的。只是确保该法案是导致最终确立国家的原因。
卡尔·马纳斯特

我的观点是,无论您在Assert-Not部分中放置什么内容,都已经隐含在Arrange部分中,因为Arrange部分中的代码已经过全面测试,并且您已经知道它的作用。
Marcel Valdez Orozco,2012年

但是我认为,Assert-Not部分具有价值,因为您说的是:鉴于Arrange部分将“世界”留在“此状态”,那么我的“行为”将使“世界”留在此“新状态” ; 如果“编配”部分所依赖的代码的实现发生变化,那么测试也会中断。但是同样,这可能与DRY背道而驰,因为您(应该)还对Arrange部分中依赖的任何代码进行了测试。
Marcel Valdez Orozco,2012年

也许在有多个团队(或一个大团队)在同一个项目上的项目中,这样的子句将非常有用,否则我认为它是不必要和多余的。
Marcel Valdez Orozco,2012年

在集成测试,系统测试或验收测试中,这样的条款可能会更好,因为“安排”部分通常取决于多个组件,并且还有更多因素可能导致“世界”的初始状态发生意外更改。但是我在单元测试中看不到它的位置。
Marcel Valdez Orozco,2012年

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包含在内的测试...我们期望什么-包含范围中的异常或范围未更改?

无论如何,关键是您要检验的行为中是否有任何假设,将其置于自己的测试中,是吗?


0

我用:

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

因为干净的安装非常重要。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.