【问题标题】:What is the role of asserts in C++ programs that have unit tests?断言在具有单元测试的 C++ 程序中的作用是什么?
【发布时间】:2011-02-05 10:41:23
【问题描述】:

我一直在向一些遗留的 C++ 代码添加单元测试,并且我遇到过许多情况,其中函数内部的断言在单元测试运行期间会被触发。我遇到的一个常见习惯用法是函数接受指针参数并在参数为 NULL 时立即断言。

我可以通过在单元测试时禁用断言来轻松解决这个问题。但是我开始怀疑单元测试是否应该减轻对运行时断言的需求。这是一个正确的评估吗?单元测试是否应该通过在管道中更快地发生来替换运行时断言(即:错误在失败的测试中而不是在程序运行时被捕获)。

另一方面,我不喜欢在代码中添加软失败(例如if (param == NULL) return false;)。运行时断言至少可以在单元测试遗漏错误的情况下更轻松地调试问题。

【问题讨论】:

  • 请注意,在 Windows 上,Boost.Test 允许将断言作为异常捕获,因此触发的断言通常只会使测试失败,也可以被捕获,例如不失败。

标签: c++ unit-testing assert


【解决方案1】:

运行时断言至少可以在单元测试遗漏错误的情况下更容易地调试问题。

这是一个非常基本的观点。单元测试并不是要取代断言(恕我直言,这是生成高质量代码的标准部分),它们是为了补充它们。

其次,假设您有一个函数Foo,它断言它的参数是有效的。
Foo 的单元测试中,您可以确保只提供有效的参数,因此您认为自己没问题。
6 个月后,其他开发人员将从一些新代码(可能有也可能没有单元测试)中调用 Foo,到那时你会非常感激你把这些断言放在那里。

【讨论】:

    【解决方案2】:

    如果你的单元测试代码是正确的,那么断言就是单元测试发现的错误。

    但是你的单元测试代码更有可能违反了它测试的单元的约束——你的单元测试代码是错误的!

    评论员提出以下观点:

    考虑一个单元测试来验证函数是否正确处理无效输入。

    断言是程序员通过中止程序来处理无效输入的方式。通过中止,程序正常运行。

    断言仅在调试版本中(如果定义了 NDEBUG 宏,则不会编译它们),并且测试程序在发布版本中是否执行了一些合理的操作很重要。考虑在发布版本上运行无效参数单元测试。

    如果您想要两全其美 - 在调试版本中检查断言触发 - 那么您希望您的线程内单元测试工具能够捕获这些断言。您可以通过提供自己的 assert.h 而不是使用系统来做到这一点;一个宏(你想要 __LINE__ 和 __FILE__ 和 ##expr)会调用你编写的函数,当在单元测试工具中运行时,它可以抛出一个自定义的 AssertionFailed。这显然不会捕获编译到您链接的其他二进制文件中但没有使用您的自定义断言器从源代码编译的断言。 (比起提供您自己的abort(),我建议您这样做,但这是您可能考虑实现相同目的的另一种方法。)

    【讨论】:

    • 我没有给你投反对票,但我认为问题在于有效的单元测试可以触发断言。考虑一个单元测试来验证函数是否正确处理无效输入。断言将触发,因为输入无效,但单元测试非常适合测试这种情况。
    • 很可能是断言暴露了单元测试中的错误。顺便说一句,我没有对你投反对票。
    • 您能否详细说明为什么您认为单元测试更有可能违反它正在测试的代码的约束?如果单元测试只是简单地调用一个类函数并触发一个断言,这是否表明正在测试的代码可能有问题?
    • @lhumongous 如果你正在测试一个函数如何处理“无效参数”,你不能抱怨程序员为了捕捉它而放入了断言。断言用于提早注意妥协的约束。稍有偏差,我认为用于测试非法参数的单元测试不如 fuzzer 和其他系统测试有用。使用 -Wall 编译,使用 lint/coverity 进行扫描,在 valgrind/purify 中运行您的程序,使用断言乱扔垃圾,然后进行无限猴子测试。这就是你如何吸出虫子的方法。
    【解决方案3】:

    当您的代码使用不正确(违反其约束或先决条件 - 使用未初始化的库、将 NULL 传递给不接受它的函数等)时,断言会捕获。

    单元测试验证您的代码是否正确使用,只要它被正确使用。

    当您的代码进入您认为不可能的状态时,断言也会捕获(因为它被认为是不可能的,所以不能进行单元测试)。

    有趣的是,至少有一个 C++ 单元测试框架 (Google Test) 支持 death tests,这些单元测试可以验证您的断言是否正常工作(这样您就可以知道您的断言正在执行捕获无效程序状态的工作) )。

    【讨论】:

    • 如果一个函数不接受 NULL,不应该用引用而不是指针来调用它吗?除非我们在谈论 C。
    • @KNoodles: char* 或 const char*;遵守现有的编码风格或编码标准; typedef void * HANDLE...
    • 断言用于捕捉 internal 不一致,而不是无效输入。例外是针对特殊情况。单元测试旨在检查两个函数的正确功能和正确的错误处理。不挑战潜在错误输入的单元测试无法提供良好的覆盖率
    【解决方案4】:

    IMO 断言和单元测试是相当独立的概念。两者都不能替代另一个。

    断言旨在确保某些条件/不变量在程序的生命周期内始终有效。或者更准确地说,为了确保如果这样的条件被打破,我们会尽快了解它,尽可能接近问题的根本原因。

    单元测试旨在确保代码的某些部分与程序的其余部分独立正常工作。

    你不能通过对一个类进行单元测试来确保它的环境在现实生活中总是会履行它的契约部分。更重要的是,所述环境包括未来的开发人员,他们可能不知道管理此类使用的接口契约(无论是隐含的还是仔细记录的)。还有许多其他软件和硬件组件,这些组件可能随时以这种特定程序的开发人员无法控制的方式更改和/或损坏。

    【讨论】:

      【解决方案5】:

      通常,您不应触发断言,因为它们应该捕捉“不可能的情况”。如果断言触发,那应该表明存在错误。

      此规则的一个例外是许多开发人员使用断言来验证参数是否有效。这没关系,前提是还有一个没有断言的构建的备份:

      assert(arg != 0);
      if (arg != 0)
          throw std::runtime_error();
      

      这样,如果一个不好的论点只发生在特定条件下(即在现场),它仍然会被抓住。

      如果您以这种方式编码,您可以关闭断言并编写否定测试以确保错误的参数被捕获。

      【讨论】:

      • 如果你想要那样(我不顺便说一句),为什么不简单地定义你的断言来抛出一个 std::runtime_error() 如果 DEBUG 没有定义?
      • @ViktorSehr - 因为 assert 的行为是标准化的,我认为改变行为会令人困惑。
      • 在这种情况下,不能使用包装器吗?还是不管构建设置都抛出运行时错误?
      【解决方案6】:

      这里有两种可能性:

      1) 当输入为空时,函数的行为被定义(由它自己的接口显式定义,或由项目的一般规则定义)。因此,单元测试需要测试这种行为。因此,您需要一个处理程序来运行运行测试用例的进程,并且该处理程序会验证代码是否触发了断言并中止,或者您需要以某种方式模拟assert

      2) 当输入为空时,函数的行为没有定义。因此,单元测试不需要传入 null - 测试也是代码的客户端。如果没有什么特别应该做的事情,你就不能测试它。

      没有第三个选项,“函数在传递 null 输入时具有未定义的行为,但测试无论如何都会传递 null,以防万一发生有趣的事情”。所以我看不出“我可以通过在单元测试时禁用断言来轻松解决这个问题”有什么帮助。当然,单元测试会导致被测函数取消引用空指针,这并不比触发断言更好。断言存在的全部原因是为了阻止更糟糕的事情发生。

      在您的情况下,也许 (1) 适用于 DEBUG 构建,(2) 适用于 NDEBUG 构建。因此,也许您可​​以仅在调试版本上运行空输入测试,并在测试发布版本时跳过它们。

      【讨论】:

        【解决方案7】:

        我只使用断言来检查“永远不会发生”的事情。如果断言触发,则说明某处出现了编程错误。

        假设一个方法采用输入文件的名称,单元测试将一个不存在的文件的名称提供给它,以查看是否抛出“找不到文件”异常。这不是“永远不会发生”的事情。 可能在运行时找不到文件。我不会在方法中使用断言来验证是否找到了文件。

        但是,文件名参数的字符串长度绝不能为负数。如果是,那么某处存在错误。所以,我可能会用一个断言说“这个长度永远不会是负数”。 (这只是一个人为的例子。)

        在你的问题的情况下,如果函数断言!= NULL,要么单元测试是错误的,不应该发送 NULL,因为这永远不会发生,或者,单元测试是有效的并且 NULL 可能是发送进来,并且该函数是错误的,不应该断言 != NULL 并且必须改为处理该条件。

        【讨论】:

          【解决方案8】:

          就我个人而言,我不倾向于使用断言,正如您所发现的,它们通常不能很好地与单元测试配合使用。我倾向于在其他人经常使用断言的情况下抛出异常。这些检查以及失败时引发的异常都存在于调试和发布版本中,我发现即使在发布版本中它们也经常捕获“不可能发生”的事情(断言通常不会像他们那样)重新经常编译出来)。我发现它对我来说效果更好,这意味着我可以编写单元测试,期望在无效输入上引发异常,而不是期望触发断言。

          很多人不同意,请参阅12etc,但我不在乎。避免断言和使用异常对我来说效果很好,并帮助我为客户生成健壮的代码......

          【讨论】:

            【解决方案9】:

            首先,要使单元测试达到assert(或在 Windows 版本上为 ASSERT_ASSERT_ASSERTE),单元测试需要运行代码正在使用调试版本进行测试。

            我想这很容易在开发人员的机器上发生。对于我们的夜间构建,我们仅在发布配置中运行单元测试,因此无需担心那里的断言。

            第二,可以拿normative approach with asserts--

            断言旨在确保某些条件/不变量是 在程序的生命周期内始终有效。或者更多 精确,以确保如果这样的条件被打破,我们可以 尽快了解它,尽可能接近问题的根本原因 可能。

            在这种情况下,任何单元测试都不应该引发断言,因为以引发断言的方式调用代码是不可能的。

            可以采用带有断言的“务实”方法:

            让开发人员在“不要这样做”和“未实现”的场景中到处使用 ASSERT。 (我们可以整天争论这是错误的™还是正确的™,但这不会使功能交付。)

            如果您采用务实的方法,那么单元测试命中断言意味着单元测试以代码不完全支持的方式调用代码。这可能意味着代码在发布构建中“什么都不做”,或者可能意味着代码在发布构建中崩溃,或者可能意味着代码做了“一些有趣的事情”。

            这是我已知使用的选项:

            • 如果断言伴随着额外的检查以使调用“无害”,则进行单元测试测试断言(在调试中)和发布中的“无害”条件。
            • 对于崩溃或“有趣的事情”,要么没有有意义的单元测试,要么您可以进行“仅调试”单元测试来测试您是否真的得到了一个断言(尽管我不太确定这是否有用) .

            【讨论】:

              【解决方案10】:

              1- Assert 是在算法开发过程中阐明不变量(循环不变量)的好方法。它对于“可读性”和调试很有用。 Bertrand Meyer 的 Eiffel 语言有一个关键字invariant。在其他语言中,assert 可用于此目的。

              2- Assert 也可以在其他情况下用作开发过程中的中间解决方案,并在代码完成时逐渐删除。即作为 TODO 项。一些asserts(那些不在上面#1 中的)需要用异常处理等替换。如果所有这些检查都显示为断言,则更容易发现它们。

              3- 在某些情况下(例如在开发新算法时),我有时会使用它来阐明没有类型检查系统(Python 和 JavaScript)的语言中的输入类型。不确定这是否是推荐的做法。和第 1 项一样,它是关于提高程序的可读性。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2023-03-10
                • 2019-04-26
                • 1970-01-01
                • 1970-01-01
                • 2015-04-07
                • 1970-01-01
                • 1970-01-01
                • 2014-07-22
                相关资源
                最近更新 更多