【问题标题】:Is Assert.Fail() considered bad practice?Assert.Fail() 被认为是不好的做法吗?
【发布时间】:2026-02-20 12:45:01
【问题描述】:

我在做 TDD 时经常使用 Assert.Fail。我通常一次只做一个测试,但是当我对以后想要实现的东西有想法时,我会快速编写一个空测试,其中测试方法的名称表明我想要实现的内容作为待办事项列表。为了确保我不会忘记我在正文中放了一个 Assert.Fail()。

在试用 xUnit.Net 时,我发现他们没有实现 Assert.Fail。当然,您始终可以 Assert.IsTrue(false) 但这也不能传达我的意图。我的印象是 Assert.Fail 不是故意实施的。这被认为是不好的做法吗?如果有,为什么?


@Martin Meredith 这不正是我所做的。我确实先编写一个测试,然后实现代码以使其工作。通常我会同时考虑几个测试。或者当我在做其他事情时,我会考虑写一个测试。那时我写了一个空的失败测试来记住。到我开始编写测试时,我会整齐地先测试。

@Jimmeh 这看起来是个好主意。忽略的测试不会失败,但它们仍会显示在单独的列表中。必须尝试一下。

@马特豪威尔斯 好想法。在这种情况下,NotImplementedException 比 assert.Fail() 更能传达意图

@米奇小麦 这就是我一直在寻找的。似乎它被遗漏了以防止它以另一种方式被滥用。

【问题讨论】:

  • 关于 Stack Overflow 的第一个 xUnit.net 问题。我想我有点哽咽了。 :)
  • 我能说什么.. 我喜欢这个项目。除了缺少 Assert.Fail :-)

标签: unit-testing xunit.net


【解决方案1】:

对于这种情况,我不调用 Assert.Fail,而是执行以下操作(在 C#/NUnit 中)

[Test]
public void MyClassDoesSomething()
{
    throw new NotImplementedException();
}

它比 Assert.Fail 更明确。

似乎普遍认为使用比 Assert.Fail() 更明确的断言更可取。大多数框架都必须包含它,因为它们没有提供更好的选择。例如,NUnit(和其他)提供了一个 ExpectedExceptionAttribute 来测试某些代码是否引发了特定类的异常。但是,为了测试异常的属性是否设置为特定值,不能使用它。相反,您必须求助于 Assert.Fail:

[Test]
public void ThrowsExceptionCorrectly()
{
    const string BAD_INPUT = "bad input";
    try
    {
        new MyClass().DoSomething(BAD_INPUT);
        Assert.Fail("No exception was thrown");
    }
    catch (MyCustomException ex)
    {
         Assert.AreEqual(BAD_INPUT, ex.InputString); 
    }
}

xUnit.Net 方法 Assert.Throws 无需 Assert.Fail 方法就使这变得更加整洁。通过不包含 Assert.Fail() 方法,xUnit.Net 鼓励开发人员寻找和使用更明确的替代方案,并在必要时支持创建新断言。

【讨论】:

  • 我支持'throw new NotImplementedException()'。
  • throws 方法允许使用 lamdbas,我非常喜欢它:MyException ex = Assert.Throws(() => action()); Assert.Equal("foobar", ex.Message);
  • 就个人而言,在我“记录未来测试”的场景中,我只会编写测试并让它通过,但它的空正文提醒我在这里要进行测试。或者我什至将测试名称/描述作为注释留在测试文件中。不过,在这里失败会很糟糕。
  • 当然,未实现的测试失败是正确的,因为根据定义,如果您还没有编写测试,那么您就不能编写代码。如果测试失败破坏了您的 CI,您也可以在其上添加一个 [Ignore] 属性。
  • 唐完全正确。失败的“未来测试占位符”会扼杀 TDD 节奏。一次做一件事……未来的测试占位符是对以后的提醒,而不是现在要做的事情。
【解决方案2】:

这是故意遗漏的。这是 Brad Wilson 关于为什么没有 Assert.Fail() 的回复:

实际上,我们并没有忽视这一点。一世 find Assert.Fail 是一个拐杖,它 意味着可能存在一个 断言缺失。有时只是 测试的结构方式,以及 有时是因为 Assert 可以 使用另一个断言。

【讨论】:

    【解决方案3】:

    我一直使用 Assert.Fail() 来处理通过简单值比较之外的逻辑检测到测试应该失败的情况。举个例子:

    try 
    {
      // Some code that should throw ExceptionX
      Assert.Fail("ExceptionX should be thrown")
    } 
    catch ( ExceptionX ex ) 
    {
      // test passed
    }
    

    因此,框架中缺少 Assert.Fail() 对我来说似乎是一个错误。我建议修补 Assert 类以包含 Fail() 方法,然后将补丁提交给框架开发人员,并附上您添加它的理由。

    至于您在工作空间中创建故意失败的测试的做法,以提醒自己在提交之前实施它们,这对我来说似乎是一个很好的做法。

    【讨论】:

    • 对于这个例子,我可能会使用大多数框架通用的 ExpectedExceptionAttribute 来节省 try catch 中的嵌套,尽管如果你想测试使用了正确的异常消息,这种方法是很好的。
    • 顺便说一句,我认为 Brad Wilson 会亲自阻止任何补丁尝试将 Fail 方法添加到 Assert 类。 ;)
    • 我在 jUnit 中经常这样做。在我看来,它非常整洁和优雅。 NUnit 中的 ExpectedExceptionAttribute 似乎消除了对 Assert.Fail() 的这种特殊需求,但我们发现一个有效用途这一事实表明可能还有其他用途。
    • 根据他的解释,没关系,这是他的代码。如果他礼貌地说“谢谢,但不,这就是原因”,那就更好了。至少他会得到反馈,表明有人对他的工作足够关心并想要改进它,即使改进不是他想要的。
    • 是的,我们不愿意包含 Fail()。值得一提的是,在使用该框架的 18 个月(以及公开发布的 13 个月)中,没有人要求使用 Fail(),也没有人为它提交过补丁。我认为我们的推理是合理的。
    【解决方案4】:

    我使用 MbUnit 进行单元测试。他们可以选择忽略测试,这些测试在测试套件中显示为橙色(而不是绿色或红色)。也许 xUnit 有类似的东西,这意味着您甚至不必在方法中添加任何断言,因为它会以令人讨厌的不同颜色显示,很难错过?

    编辑:

    在 MbUnit 中是这样的:

    [Test]
    [Ignore]
    public void YourTest()
    { } 
    

    【讨论】:

      【解决方案5】:

      这是我在为我想通过设计抛出异常的代码编写测试时使用的模式:

      [TestMethod]
      public void TestForException()
      {
          Exception _Exception = null;
      
          try
          {
              //Code that I expect to throw the exception.
              MyClass _MyClass = null;
              _MyClass.SomeMethod();
              //Code that I expect to throw the exception.
          }
          catch(Exception _ThrownException)
          {   
              _Exception = _ThrownException
          }
          finally
          {
              Assert.IsNotNull(_Exception);
              //Replace NullReferenceException with expected exception.
              Assert.IsInstanceOfType(_Exception, typeof(NullReferenceException));
          }
      }
      

      恕我直言,与使用 Assert.Fail() 相比,这是测试异常的更好方法。这样做的原因是,我不仅要测试抛出的异常,还要测试异常类型。我意识到这与 Matt Howells 的答案相似,但恕我直言,使用 finally 块更健壮。

      显然,仍然可以包含其他 Assert 方法来测试异常输入字符串等。我会感谢您的 cmets 和对我的模式的看法。

      【讨论】:

        【解决方案6】:

        就我个人而言,只要您最终在实现要通过的代码之前开始编写测试,就可以将测试套件用作这样的待办事项列表。

        话虽如此,我自己曾经使用过这种方法,尽管现在我发现这样做会导致我走上一条预先编写太多测试的道路,这以一种奇怪的方式就像不编写测试的相反问题完全没有:恕我直言,您最终对设计做出决定还为时过早。

        顺便说一下,在 MSTest 中,标准测试模板在其样本末尾使用 Assert.Inconclusive。

        AFAIK xUnit.NET 框架旨在非常轻量级,是的,他们确实故意削减了失败,以鼓励开发人员使用明确的失败条件。

        【讨论】:

        • 我同意这样做太多会导致 YAGNI 情况 :-) 谢谢。
        【解决方案7】:

        大胆猜测:保留 Assert.Fail 旨在阻止您认为编写测试代码的好方法是一大堆意大利面条,导致在坏情况下出现 Assert.Fail。 [编辑添加:其他人的答案大致证实了这一点,但引用了]

        由于这不是您正在做的事情,xUnit.Net 可能会过度保护。

        或者他们只是认为它是如此罕见且如此不正交以至于没有必要。

        我更喜欢实现一个名为 ThisCodeHasNotBeenWrittenYet 的函数(实际上更短一些,以便于输入)。无法比这更清楚地传达意图,而且您有一个精确的搜索词。

        无论是失败,还是没有实现(引发链接器错误),或者是一个无法编译的宏,都可以根据您当前的偏好进行更改。例如,当您想要运行 已完成的东西时,您想要失败。当您坐下来摆脱它们时,您可能想要一个编译错误。

        【讨论】:

          【解决方案8】:

          使用我通常做的好代码:

          void goodCode() {
               // TODO void goodCode()
               throw new NotSupportedOperationException("void goodCode()");
          }
          

          使用我通常做的测试代码:

          @Test
          void testSomething() {
               // TODO void test Something
               Assert.assert("Some descriptive text about what to test")
          }
          

          如果使用JUnit,又不想得到失败,而是报错,那么我通常会这样做:

          @Test
          void testSomething() {
               // TODO void test Something
               throw new NotSupportedOperationException("Some descriptive text about what to test")
          }
          

          【讨论】:

            【解决方案9】:

            当心Assert.Fail 及其破坏性影响,使开发人员编写愚蠢或损坏的测试。例如:

            [TestMethod]
            public void TestWork()
            {
                try {
                    Work();
                }
                catch {
                    Assert.Fail();
                }
            }
            

            这很愚蠢,因为 try-catch 是多余的。如果抛出异常,则测试失败。

            还有

            [TestMethod]
            public void TestDivide()
            {
                try {
                    Divide(5,0);
                    Assert.Fail();
                } catch { }
            }
            

            这被破坏了,无论 Divide 函数的结果如何,测试都将始终通过。同样,当且仅当它抛出异常时,测试才会失败。

            【讨论】:

            • Smartarse:一些异常(例如 *Exception)无法被捕获。
            【解决方案10】:

            如果您正在编写一个刚刚失败的测试,然后为它编写代码,然后再编写测试。这不是测试驱动开发。

            从技术上讲,如果您正确使用测试驱动开发,则不需要 Assert.fail()。

            您是否考虑过使用待办事项列表,或将 GTD 方法应用于您的工作?

            【讨论】:

            • 如果您尝试测试负面行为怎么办?
            • 您可以轻松地在 Assert.IsTrue 或 Assert.IsFalse 中使用布尔表达式来表示消极或积极的行为。
            • 提问者没有说他在完成测试之前正在编写代码。他说他在完成这些测试之前正在编写其他测试。这可能是无纪律的,也可能不是,但我不认为这违反了 TDD。
            【解决方案11】:

            MS Test 有 Assert.Fail(),但它也有 Assert.Inconclusive()。我认为 Assert.Fail() 最合适的用途是,如果你有一些内联逻辑,放在断言中会很尴尬,尽管我什至想不出任何好的例子。在大多数情况下,如果测试框架支持 Assert.Fail() 以外的东西,那么就使用它。

            【讨论】:

              【解决方案12】:

              我认为你应该问问自己(前期)测试应该做什么。

              首先,您编写了一个(一组)测试而没有实现。 也许,还有下雨天的场景。

              所有这些测试都必须失败,才能成为正确的测试: 所以你想要实现两件事: 1)验证你的实现是否正确; 2) 验证您的单元测试是否正确。

              现在,如果您进行前期 TDD,您希望执行所有测试,包括 NYI 部分。 在以下情况下,您的总测试运行结果通过: 1)所有实现的东西都成功了 2) 所有 NYI 的东西都失败了

              毕竟,如果您的单元测试成功而没有实现,那将是单元测试的省略,不是吗?

              您希望收到一封持续集成测试的邮件,检查所有已实施和未实施的代码,并在任何已实施的代码失败或任何未实施的代码成功时发送。两者都是不希望的结果。

              只写一个 [ignore] 测试不会完成这项工作。 两者都不是,一个断言会停止第一个断言失败,而不是在测试中运行其他测试行。

              现在,如何实现呢? 我认为它需要一些更高级的测试组织。 并且它需要一些其他机制然后断言来实现这些目标。

              我认为您必须拆分测试并创建一些完全运行但必须失败的测试,反之亦然。

              想法是将测试拆分为多个程序集,使用测试分组(mstest 中的有序测试可能会完成这项工作)。

              不过,如果 NYI 部门中的所有测试都未通过,那么通过邮件发送 CI 构建并不容易且直截了当。

              【讨论】:

                【解决方案13】:

                为什么要使用Assert.Fail 来表示应该抛出异常?那是不必要的。为什么不直接使用ExpectedException 属性?

                【讨论】:

                • 我认为您误读了这个问题。我不期待例外。我将测试用作稍后要编写的测试的占位符。我还没有测试任何东西,所以我想提醒一下测试失败。
                【解决方案14】:

                这是我们 Assert.Fail() 的用例。

                我们的单元测试的一个重要目标是它们不接触数据库

                有时模拟没有正确发生,或者应用程序代码被修改并且无意中进行了数据库调用。

                这可能在调用堆栈中很深。异常可能会被捕获,因此它不会冒泡,或者因为测试最初是使用数据库运行的,所以调用会起作用。

                我们所做的是向单元测试项目添加一个配置值,以便在第一次请求数据库连接时我们可以调用 Assert.Fail("Database used");

                Assert.Fail() 全局作用,甚至在不同的库中。因此,这可以作为所有单元测试的包罗万象。

                如果其中任何一个在单元测试项目中命中数据库,那么它们将失败。

                因此我们失败得很快。

                【讨论】: