我一直在为一些遗留的C ++代码添加单元测试,并且我遇到了许多场景,其中函数内的断言将在单元测试运行期间被触发。我遇到的一个常见习语是带有指针参数的函数,如果参数为NULL,则立即断言。
当我进行单元测试时,我可以通过禁用断言轻松解决这个问题。但我开始怀疑单元测试是否应该减轻对运行时断言的需求。这是正确的评估吗?单元测试是否应该通过在管道中更快地发生来替换运行时断言(即:错误是在失败的测试中捕获而不是在程序运行时捕获的。)
另一方面,我不喜欢添加软代码失败(例如if (param == NULL) return false;
)。运行时断言至少可以在单元测试错过错误时更容易调试问题。
答案 0 :(得分:16)
运行时断言至少可以在单元测试错过错误时更容易调试问题。
这是一个非常基本的观点。单元测试并不是要取代断言(恕我直言是制作高质量代码的标准部分),它们是为了补充它们。
其次,假设您有一个函数Foo
,它断言它的参数是有效的
在Foo
的单元测试中,您可以确保只提供有效参数,因此您认为自己没问题。
在轨道上下来6个月,其他一些开发人员将从一些新的代码中调用Foo
(可能有也可能没有单元测试),那时你会非常感激你把那些断言放在那里
答案 1 :(得分:9)
如果你的单元测试代码是正确的,那么断言就是单元测试发现的错误。
但是你的单元测试代码更有可能违反了它测试的单位的限制 - 你的单元测试代码是错误的!
评论员提出了以下观点:
考虑一个单元测试,验证函数是否正确处理无效输入。
断言是程序员通过中止程序处理无效输入的方式。通过中止,该程序正常运行。
断言仅在调试版本中(如果定义了NDEBUG
宏,则不会编译它们)并且测试程序在发布版本中做了哪些事情是很重要的。考虑在发布版本上运行无效参数unit-test。
如果你想要两个世界 - 在调试版本中检查断言 - 那么你希望你的线程内单元测试工具捕获这些断言。您可以通过提供自己的 assert.h 而不是使用系统来实现此目的;一个宏(你想要__LINE__和__FILE__和## expr)会调用你编写的函数,这可以在单元测试工具中运行时抛出自定义AssertionFailed。这显然不会捕获编译到您链接的其他二进制文件中的断言,但不会使用您的自定义断言器从源代码编译。 (我建议你提供自己的abort()
,但这是你可能考虑达到同样目的的另一种方法。)
答案 2 :(得分:4)
当你的代码被错误地使用时会断言(违反其约束或前提条件 - 使用没有初始化的库,将NULL传递给不接受它的函数等)。
单元测试验证您的代码是否正确,只要它正确使用。
当您的代码进入您认为不可能的状态时,断言也会被捕获(因为它被认为是不可能的,不能进行单元测试)。
有趣的是,至少有一个C ++单元测试框架(Google Test)支持death tests,它们是单元测试,用于验证您的断言是否正常工作(这样您就可以知道您的断言正在完成它们的工作捕获无效的程序状态。)
答案 3 :(得分:3)
IMO断言和单元测试是相当独立的概念。它们都不能取代另一个。
断言旨在确保某些条件/不变量在程序的生命周期内始终有效。或者更确切地说,为了确保如果这种情况被破坏,我们尽快了解它,尽可能接近问题的根本原因。
单元测试旨在确保代码的某些部分与程序的其余部分正确地隔离。
您不能通过单元测试一个类来确保其环境总是在现实生活环境下完成其合同的一部分。更重要的是,所述环境包括未来的开发人员,他们可能不了解管理此类使用的接口契约(无论是隐式还是仔细记录)。还有许多其他软件和硬件组件,它们可能会以某种方式随时更改和/或被破坏。这个特定程序的开发人员无法控制。
答案 4 :(得分:3)
通常情况下,你不应该绊倒断言,因为它们应该捕捉“不可能的情况”。如果一个断言触发,则表明存在错误。
此规则的一个例外是许多开发人员使用断言来验证参数是否有效。如果还有没有断言的构建备份,这是可以的:
assert(arg != 0);
if (arg != 0)
throw std::runtime_error();
这样,如果一个不好的参数只在特定条件下发生(即在现场外),它仍然会被捕获。
如果以这种方式编码,可以关闭断言并编写否定测试以确保捕获错误的参数。
答案 5 :(得分:3)
这里有两种可能性:
1)当输入为null时,函数的行为(由其自己的接口显式定义,或由项目的一般规则定义)。因此,单元测试需要测试此行为。因此,您需要一个处理程序来运行运行测试用例的进程,并且处理程序验证代码是否触发了断言并中止,或您需要以某种方式模拟assert
。
2)当输入为空时,未定义函数的行为。因此单元测试不需要传入null - 测试也是代码的客户端。如果没有任何特别的东西是所谓的那么你就无法测试。
没有第三个选项,“该函数在传递空输入时具有未定义的行为,但测试无论如何都会传入null,以防万一有趣的事情发生”。所以我不明白,“当我进行单元测试时,通过禁用断言,我可以轻松解决这个问题”。当然,单元测试会导致被测函数取消引用空指针,这不比绊倒断言更好。断言的全部原因是要阻止更糟糕的事情发生。
在您的情况下,或许(1)适用于DEBUG构建,(2)适用于NDEBUG构建。因此,也许您只能在调试版本上运行空输入测试,并在测试版本构建时跳过它们。
答案 6 :(得分:3)
我只使用断言来检查“永远不会发生”的事情。如果一个断言触发,那么在某处就会出现编程错误。
假设一个方法采用输入文件的名称,单元测试会向其提供一个不存在的文件的名称,以查看是否抛出了“找不到文件”异常。这不是“永远不会发生”的事情。 可能无法在运行时找到文件。我不会在方法中使用断言来验证文件是否已找到。
但是,文件名参数的字符串长度绝不能为负数。如果是,那么某处就有一个bug。所以,我可能会使用一个断言说“这个长度永远不会是负面的”。 (这只是一个人为的例子。)
对于你的问题,如果函数断言!= NULL,单元测试是错误的,不应该发送NULL,因为这永远不会发生,或者,单元测试是有效的,NULL可能是发送,并且函数是错误的,不应该断言!= NULL并且必须改为处理该条件。
答案 7 :(得分:1)
我个人并不倾向于使用断言,正如您所发现的那样,他们通常不能很好地使用单元测试。我倾向于在其他人经常使用断言的情况下抛出异常。调试版本和发布版本中都存在这些检查以及失败时抛出的异常,我发现它们经常捕获“即使在发布版本中也不可能发生”的事情(它们通常不会像它们那样断言)经常编译出来)。我发现它对我来说效果更好,这意味着我可以编写单元测试,期望将异常抛出到无效输入上,而不是期望触发一个断言。
有很多人不同意,请参阅1,2,etc,但我不在乎。避免断言和使用异常对我来说效果很好,并帮助我为客户生成强大的代码......
答案 8 :(得分:0)
首先,对于在Windows版本上点击assert
(或ASSERT
或_ASSERT
或_ASSERTE
)的单元测试,单元测试需要使用调试版本运行测试中的代码。
我想这很容易发生在开发人员的机器上。对于我们的夜间构建,我们只在发布配置中运行单元测试,因此不必担心那里的断言。
第二个,可以使用normative approach with asserts -
断言是为了确保某些条件/不变量 在程序的生命周期内始终有效。或者更多 确切地说,为了确保如果这种情况被打破,我们就会这样做 尽快了解它,尽可能接近问题的根本原因 可能的。
在这种情况下,没有单元测试应该引发一个断言,因为以一种断言被引发的方式调用代码是不可能的。
或可以采用断言的“实用”方法:
让开发人员为“不要这样做”和“未实现”的场景洒遍ASSERT。 (而且我们可以整天争论这是错误的还是正确的,但这不会得到提供的功能。)
如果采用实用方法,则单元测试命中断言意味着单元测试以代码不支持完全的方式调用代码。它可能意味着代码在发布版本中“什么也不做”,或者它可能意味着代码在发布版本中崩溃,或者它可能意味着代码“做了一些有趣的事情”。
以下是我所知道的选项:
答案 9 :(得分:0)
1- 断言是在算法开发过程中澄清不变量(循环不变量)的好方法。它可以用于"可读性"以及调试。 Bertrand Meyer的语言Eiffel有一个关键字 invariant 。在其他语言中,断言可用于此目的。
2-
断言也可以在其他情况下用作开发期间的中间解决方案,并在代码完成时逐渐删除。也就是说,作为TODO项目。一些assert
s(不在上面第1项中的那些)需要被异常处理等替换。如果所有这些检查都显示为断言,则更容易发现它们。
3- 在某些情况下(例如在开发新算法时),我有时会用它来澄清没有类型检查系统(Python和JavaScript)的语言中的输入类型。不确定是否是推荐的做法。与第1项一样,它是关于提高程序的可读性。