在对此出色帖子的评论中,Roy Osherove提到了OAPT项目,该项目旨在在单个测试中运行每个断言。

以下内容写在项目的主页上: >应该在每个单元中使用一个断言
测试。


而且,Roy在评论中写道:通常您要测试
每次测试一个逻辑概念。您可以
在同一个对象上具有多个断言。它们通常是相同的概念。


我认为,在某些情况下需要多个断言(例如Guard Assertion),但总的来说,我会尽量避免这种情况。你有什么意见?请提供一个真实的示例,其中确实需要多个断言。

评论

在没有多个断言的情况下如何进行模拟?对模拟的每个期望本身就是一个断言,包括您施加的任何调用顺序。

我已经看到了过去滥用一种方法的哲学。一个老同事使用时髦的继承机制来实现这一点。它导致了许多子类(每个分支一个)和许多测试,它们执行相同的设置/拆卸过程,只是为了检查不同的结果。它很慢,难以阅读,并且存在严重的维护问题。我从未说服他改回更经典的方法。 Gerard Meszaros的书详细讨论了这个主题。

我认为,作为一般经验法则,您应尽量减少每次测试的断言数量。但是,只要测试足以将问题缩小到代码中的特定位置,那么它就是有用的测试。

我已经看到了使用多个断言代替RowTest(MbUnit)/ TestCase(NUnit)来测试各种边缘情况行为的情况。使用适当的工具完成工作! (不幸的是,MSTest似乎还没有行测试功能。)

@GalacticCowboy您可以使用测试数据源获得PreTest和Test Case的类似功能。我正在使用一个非常成功的简单CSV文件。

#1 楼

我认为这不一定是一件坏事,但我确实认为我们应该努力在测试中只包含单个断言。这意味着您编写了更多的测试,而我们的测试最终一次只能测试一件事。我认为只有在您的测试中有大约五个或更多断言时,它才会变成代码(测试?)的味道。

如何解决多个断言?

评论


像这样的答案-即可以,但在一般情况下,则不好(-:

–墨菲
2010-09-28 11:45

嗯为什么要这么做?方法执行完全一样吗?

– jgauffin
2012年10月30日15:08

每个单元测试一个断言是测试读者向上和向下滚动能力的好方法。

–汤姆
2012年10月30日15:45

这背后有什么理由吗?照原样,此当前答案仅说明了应该是什么,但没有说明原因。

–史蒂文·杰里斯(Steven Jeuris)
15年12月22日在13:36

强烈反对。答案没有列出具有单个断言的任何优点,而明显的缺点是仅由于Internet上的答案如此,您才需要复制粘贴测试。如果您需要测试一个结果的多个字段或单个操作的多个结果,则绝对应该在独立的断言中声明所有这些字段,因为与在大的Blob声明中测试它们相比,它提供了更多有用的信息。在一个好的测试中,您将测试单个操作,而不是单个操作的结果。

–彼得
16年2月2日在12:18

#2 楼

测试仅应出于一个原因而失败,但这并不总是意味着仅应存在一个Assert语句。恕我直言,保持“安排,执行,声明”模式更为重要。
关键是您只有一个操作,然后使用断言检查该操作的结果。但这是“安排,行动,断言,测试结束”。如果您想通过执行其他操作来继续测试,然后再声明更多,请进行单独的测试。
我很高兴看到多个assert语句构成测试同一操作的一部分。例如
 [Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 
 


 [Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 
 

您可以将它们组合成一个断言,但这与坚持您应该或必须做的事情不同。合并它们没有任何改善。
例如第一个可能是
 Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 
 

但这不是更好-错误消息不那么具体,它具有没有其他优势。我敢肯定,您还会想到其他示例,其中将两个或三个(或更多)断言组合到一个大的布尔条件中将使其更难阅读,更难更改且更难弄清失败的原因。
NB:我在这里编写的代码是带有NUnit的C#,但是该原理将适用于其他语言和框架。语法也可能非常相似。

评论


关键是您只有一个操作,然后使用断言检查该操作的结果。

–阿弥陀佛
2015年8月9日12:00

我认为,如果Arrange耗费时间,则拥有更多的Assert也很有趣。

– Rekshino
17年5月12日在7:18

@Rekshino,如果安排很费时间,我们可以共享安排代码,例如,将安排代码放入测试初始化​​例程中。

– Shaun Luttin
18-2-24在0:43



因此,如果将我与Jaco的答案进行比较,则“仅一个断言”就变成“仅一组断言”,这对我来说更有意义。

–沃尔夫特
18年4月19日在7:42

这是一个很好的答案,但我不同意单个断言并不更好。错误的断言示例并不更好,但这并不意味着如果正确执行一个断言也不会更好。许多库都允许自定义断言/匹配器,因此可以创建一些尚不存在的东西。例如,我认为Assert.IsBetween(10,100,value)会打印出Expected 8在10和100之间,这比两个单独的断言更好。您当然可以争辩说它不是必需的,但是通常值得考虑的是,在进行整套定义之前是否容易将其简化为单个断言。

– Thor84no
18年11月15日在8:42

#3 楼

我从来没有想过有一个以上的断言是一件坏事。

我一直都这样做: public void ToPredicateTest() { ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100); Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400) .Or() .OpenParenthesis() .GreaterThanConst(500) .And() .LessThanConst(1000) .And().Not() .EqualsConst(666) .CloseParenthesis() .ToPredicate(); Assert.IsTrue(p(ResultField.FillResult(rf, 399))); Assert.IsTrue(p(ResultField.FillResult(rf, 567))); Assert.IsFalse(p(ResultField.FillResult(rf, 400))); Assert.IsFalse(p(ResultField.FillResult(rf, 666))); Assert.IsFalse(p(ResultField.FillResult(rf, 1001))); Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate(); Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true))); Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false))); }

这里我使用多个断言来确保可以将复杂的条件转换为预期的谓词。

我仅测试一个单元(ToPredicate方法) ,但我涵盖了测试中我能想到的所有内容。

评论


由于错误检测,多个断言是不好的。如果您的第一个Assert.IsTrue失败,则其他断言将不会执行,并且您也不会从中获取任何信息。另一方面,如果您有5个测试,而不是5个断言的1个测试,则可以得到一些有用的信息

–狡猾
2010-09-28 10:52

如果所有断言都测试同一种功能,您是否认为这仍然很糟糕?像上面一样,该示例测试条件,如果其中任何一个失败,则应对其进行修复。如果前一个断言失败,您可能会错过最后两个断言对您来说是否重要?

– cringe
2010-09-28 10:55

我一次解决一个问题。因此,测试可能多次失败的事实并没有让我感到困扰。如果我将它们分开,则会出现相同的错误,但是会同时出现。我发现一次更容易解决问题。我承认,在这种情况下,最后两个断言可能可以重构为自己的测试。

–马特·艾伦(Matt Ellen)
2010-09-28在11:00

您的案例很有代表性,这就是为什么NUnit具有附加属性TestCase的原因-nunit.org/?p=testCase&r=2.5

– Restuta
2010-09-28 11:12

google C ++测试框架具有ASSERT()和EXPECT()。 ASSERT()在失败时停止,而EXPECT()继续。当您想在测试中验证多个内容时,这非常方便。

–ratkok
2011年6月25日4:28



#4 楼

当我使用单元测试来验证高级行为时,我绝对将多个断言放入单个测试中。这是我实际上用于一些紧急通知代码的测试。测试之前运行的代码使系统进入一种状态,如果运行主处理器,则会发出警报。

 @Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}
 


它代表了过程中每个步骤都需要存在的条件,以使我确信代码的行为符合我的期望。如果一个断言失败,我不在乎其余的断言甚至不会被执行。因为系统状态不再有效,所以这些后续的断言将不会告诉我任何有价值的信息。*如果assertAllUnitsAlerting()失败,那么在确定导致错误的原因之前,我不知道对assertAllNotificationSent()的成功或失败做出什么判断。

(*-可以想象,它们可能在调试问题时很有用。但是已经收到测试失败的最重要信息。)

评论


当您这样做时,最好将测试框架与相关测试一起使用,这样会更好(例如testng支持此功能)

– emo田
2012-10-26 13:44

我也这样编写测试,以便您可以确信代码的功能和状态更改,我认为这不是单元测试,而是集成测试。

–mdma
2013年9月11日上午9:30

您对将其重构为assertAlarmStatus(int numberOfAlarmMessages);有何看法?

– Borjab
16年3月7日在16:37

您的主张将为您提供非常好的测试名称。

–比约恩
16-10-21在18:38

最好让测试运行,即使输入无效也是如此。它以这种方式为您提供了更多信息(尤其是当您不希望它通过时仍然通过)。

–窗帘狗
18年4月19日在4:47

#5 楼

以下代码描述了我认为一个方法中的多个断言不是一件坏事的另一个原因:

 class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}
 


在我的测试中,我只想测试service.process()Inner类实例中是否返回正确的数字。

而不是测试... class =“ lang-java prettyprint-override”> @Test public void test() { Result res = service.process(); if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() ); }

我正在做

 @Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
 


评论


这是一件好事,您的测试中不应包含任何条件逻辑。它使测试更加复杂且可读性较差。我猜罗伊(Roy)在他的博客文章中概述了这一点,即多数情况下,对一个对象进行多次断言是可以的。因此,您拥有的只是警惕的主张,可以拥有它们。

– Restuta
2012年10月30日20:44

您的Assert.notNulls是多余的,如果它们为null,则测试将因NPE而失败。

–萨拉
16年6月2日在10:01

此外,如果res为null,则第一个示例(带有if)将通过

–萨拉
16年6月2日在10:02

我同意@kai notNull是多余的,但是我觉得拥有断言(并且如果我也不懒于发送适当的消息)比异常更干净...

–贝塔利斯塔
16年6月2日在13:50

失败的断言也会引发异常,在这两种情况下,您都将直接链接到抛出该行的确切行,并附带一个堆栈跟踪,因此就我个人而言,我宁愿不要使用将被检查的前提条件来使测试混乱。我更喜欢一个单行的Assert.assertEquals(...,service.process()。getInner());,如果行变得“太长”,则可以提取变量

–萨拉
16年6月2日在13:53

#6 楼

我认为在很多情况下,编写多个断言在测试仅应出于一个原因而失败的规则内有效。

例如,设想一个解析日期字符串的函数:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}


如果测试失败是由于某种原因,则说明解析不正确。如果您认为该测试可能由于三个不同的原因而失败,那么恕我直言,在“一个原因”的定义中,IMHO的含义太细了。

评论


您可以在代码中看到一个日期对象,并使用3个单独的测试。您可以进行3个测试:1.期望年等于2.期望月等于3.期望日等于优点是只有这些原因会导致这些单独的测试失败:1.年分析器损坏2. month解析器损坏3. day解析器损坏最重要的是,这不是支持您的主张的好例子。

– OSGI Java
10月6日23:39



#7 楼

如果测试失败,您将不知道以下断言是否也会中断。通常,这意味着您会丢失有价值的信息以找出问题的根源。我的解决方案是使用一个断言但具有多个值:

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);


这使我可以立即查看所有失败的断言。我使用多行代码,因为大多数IDE会在比较对话框中并排显示字符串差异。

#8 楼

我不知道在[Test]方法本身中包含多个断言是一个好主意的任何情况。人们喜欢拥有多个断言的主要原因是,他们试图为每个要测试的类创建一个[TestFixture]类。相反,您可以将测试分为更多的[TestFixture]类。这使您可以查看多种方式,使代码可能无法按预期方式做出反应,而不仅仅是第一个断言失败的方式。实现此目的的方法是,每个类至少要有一个目录,并且其中包含许多[TestFixture]类。每个[TestFixture]类都将以您要测试的对象的特定状态命名。 [SetUp]方法将使对象进入类名所描述的状态。然后,您将拥有多个[Test]方法,这些方法在给定对象的当前状态的情况下断言您期望为真的不同事物。每个[Test]方法均以其声明的事物命名,除非它可能以概念命名,而不仅仅是代码的英语读出。然后,每个[Test]方法实现都只需要声明一行内容的一行代码即可。这种方法的另一个优点是,通过查看类和方法名称,您可以很清楚地了解要测试的内容和期望的内容,从而使测试变得易于阅读。当您开始意识到要测试的所有小巧情况以及发现错误时,这也将更好地扩展。通常,这意味着[SetUp]方法中的最后一行代码应存储[TestFixture]的私有实例变量中的属性值或返回值。然后,您可以使用不同的[Test]方法对此实例变量声明许多不同的内容。您还可以断言,既然处于期望状态,则将被测试对象的不同属性设置为

。有时,在使被测对象进入所需状态时,您需要沿途进行断言,以确保在使对象进入所需状态之前没有弄乱。在这种情况下,这些额外的断言应出现在[SetUp]方法内。如果[SetUp]方法内部出现问题,则很明显,在对象进入要测试的期望状态之前,测试有问题。

您可能会遇到另一个问题您可能正在测试预期会引发的异常。这可能会诱使您不遵循上述模型。但是,仍然可以通过在[SetUp]方法内捕获异常并将其存储到实例变量中来实现。这将允许您断言关于异常的不同事物,每个事物都有其自己的[Test]方法。然后,您还可以声明有关被测对象的其他信息,以确保不会因引发异常而产生意外副作用。

示例(将其分解为多个文件):

 namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
 


评论


在这种情况下,您如何处理TestCase输入?

–西蒙·吉尔比
16年2月12日在21:45

因此,在我目前的组织中,我们有20,000多个单元测试,与您显示的非常相似。这是一场噩梦。许多测试设置代码被复制/粘贴,导致错误的测试设置和无效的测试通过。对于每个[Test]方法,将重新实例化该类,然后再次执行[SetUp]方法。这会杀死.NET Garbage Collector,并导致测试运行极其缓慢:本地运行5分钟以上,在构建服务器上运行20分钟以上。 20K测试应在大约2-3分钟内运行。我根本不会推荐这种测试方式,尤其是对于大型测试套件。

–fourpastmidnight
18年7月9日在22:03

@fourpastmidnight您所说的大部分内容似乎都是有效的批评,但是关于复制和粘贴设置代码并因此出错的问题,这不是结构问题,而是不负责任的程序员(这可能是不负责任的经理或不利环境的结果)比差劲的程序员更多)。如果人们只是复制并粘贴代码并希望它是正确的并且不想打扰理解代码,那么无论出于任何原因,无论在任何情况下,他们都需要受过培训,不要这样做,或者如果不能这样做,请放手训练有素。这违背了编程的每个原则。

–still_dreaming_1
18年7月10日在21:31

但是总的来说,我同意,这是疯狂的过度杀伤力,将导致大量的膨胀/行李/重复,从而导致各种问题。我以前很疯狂,推荐这样的东西。那就是我希望能够在前一天每天对自己说的话,因为那意味着我永远不会停止寻找更好的做事方法。

–still_dreaming_1
18年7月10日在21:33



@ still_dreaming_1 wrt“不负责任的程序员”:我同意这种行为是一个主要问题。但是,无论好坏,这种测试结构的确会引起这种行为。除了不良的开发实践之外,我对这种形式的主要反对意见是,它确实破坏了测试性能。没有什么比运行缓慢的测试套件更糟糕的了。运行缓慢的测试套件意味着人们不会在本地运行测试,甚至会试图在中间版本上跳过它们-再次出现人员问题,但这种情况确实发生了-通过确保您具有快速运行的测试可以避免所有这些用。

–fourpastmidnight
18年7月11日在4:15

#9 楼

只有在测试失败时,才在同一测试中具有多个断言。然后,您可能必须调试测试或分析异常,以找出失败的断言。在每个测试中只有一个断言通常可以更容易找出问题所在。

我想不出真正需要多个断言的情况,因为您始终可以在同一断言中将它们重写为多个条件。但是,例如,如果您有几个步骤来验证步骤之间的中间数据,而不是冒着由于输入错误而使后面的步骤崩溃的风险,那么这将是更可取的。

评论


如果将多个条件组合到一个断言中,那么一旦失败,您所知道的只是一个失败。使用多个断言,您将特别了解其中的一些断言(直到失败并包括故障在内)。考虑检查返回的数组是否包含单个值:检查它是否不为null,然后它恰好具有一个元素,然后是该元素的值。 (取决于平台)仅立即检查该值可能会提供null解除引用(比null断言失败有用),并且不检查数组长度。

–理查德
2010-09-28 15:41



@Richard:获取结果,然后从结果中提取内容将是一个分几个步骤的过程,因此我在答案的第二段中进行了介绍。

–古法
10-9-28 10:42

经验法则:如果测试中有多个断言,则每个断言都应有不同的消息。那么您就没有这个问题了。

–安东尼
2012年10月26日13:17

并且使用质量测试运行程序(例如NCrunch)将在测试代码和被测代码中准确显示测试失败的那一行。

–fourpastmidnight
18年7月9日在21:52

#10 楼

如果您在一个测试功能中有多个断言,我希望它们与您正在进行的测试直接相关。例如,

 @Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}
 


进行了大量测试(即使您觉得它是可能会造成太大的伤害)不是一件坏事。您可以争辩说,进行至关重要且最基本的测试更为重要。因此,在进行断言时,请确保正确放置断言语句,而不必过多担心多个断言。如果需要多个,请使用多个。

#11 楼

单元测试的目的是为您提供有关失败原因的尽可能多的信息,而且还可以帮助您首先准确地找出最根本的问题。如果您从逻辑上知道一个断言将由于另一个断言失败而失败,或者换句话说,测试之间存在依赖关系,则可以将它们作为单个测试中的多个断言进行滚动。这样做的好处是不会在测试结果中散布明显的故障,如果我们在一次测试中对第一个断言采取行动,则可以消除这些故障。在这种关系不存在的情况下,自然会优先选择将这些断言分成单独的测试,因为否则要找到这些失败将需要多次迭代运行才能解决所有问题。

如果然后再以需要编写过于复杂的测试的方式设计单元/类,则可以减轻测试负担,并有可能促进更好的设计。

#12 楼

是的,只要失败的测试为您提供足够的信息以能够诊断失败,就可以具有多个断言。这将取决于您要测试的内容以及故障模式是什么。测试。


我从来没有发现这样的表述是有用的(类应该有一个改变的理由就是这样无益的格言的一个例子)。考虑一个断言,即两个字符串相等,这在语义上等同于断言两个字符串的长度相同,并且相应索引处的每个字符均相等。

我们可以概括地说,可以将多个断言的系统重写为单个断言,并且可以将任何单个断言分解为一组较小的断言。

因此,仅关注代码的清晰度和测试的清晰度结果,并以此指导您使用的断言数量,而不是相反。

#13 楼

这个问题与意大利面条和千层面代码问题之间的经典平衡问题有关。每个测试只有一个断言,可能会使您的测试同样难以理解,因为在一个宽面条中进行了多个测试,从而发现哪个测试无法完成什么事情。答案。

#14 楼

答案很简单-如果您测试一个函数更改了同一对象甚至两个不同对象的多个属性,并且该函数的正确性取决于所有这些更改的结果,那么您想断言所有这些更改均已正确执行!

我有一个逻辑概念,但是相反的结论是,任何函数都不能更改一个以上的对象。但是,根据我的经验,在所有情况下都无法实现。

采取银行交易的逻辑概念-在大多数情况下,从一个银行帐户中提取金额必须包括将该金额添加到另一个帐户中。您永远都不想将这两件事分开,它们形成一个原子单元。您可能要创建两个函数(withdraw / addMoney),并另外编写两个不同的单元测试。但是,这两个动作必须在一个事务中进行,并且您还想确保该事务有效。在这种情况下,仅确保单个步骤成功是远远不够的。您必须在测试中检查两个银行帐户。

可能会有更复杂的示例,您首先不会在单元测试中进行测试,而是在集成或验收测试中进行测试。但是这些边界是流畅的,恕我直言!决定并非那么容易,这取决于环境和个人喜好。从一个帐户中提取资金并将其添加到另一个帐户中仍然是一个非常简单的功能,并且绝对是单元测试的候选人。

#15 楼

我什至一般都不同意“仅出于一个原因而失败”。
更重要的是,测试要简短并且可以清楚地读取imo。当测试很复杂时,使用一个(长)描述性名称,并且测试更少的事情就更有意义。