断言和单元测试是否不兼容?

7
我有一些与测试包含assert.h中的assert宏的某些函数相关的问题。
如果assert失败,测试也会失败。 这让我有一些永远不会起作用的测试用例。
例如,一个函数而不是指示失败(返回false或类似的内容)进行断言。
是否有解决方案(单元测试包含assert的函数)?
6个回答

13

也许只有我这么认为,但如果你遇到了断言失败,你甚至不应该考虑进行更高级别的单元测试,直到你把它们修好为止。这意味着,如果代码编写正确,无论是在单元测试中还是其他情况下,断言都永远不应该失败。至少这是我编写代码的方式。


17
断言可以用来验证输入参数。一个有效的单元测试应该是“这个函数在遇到某些类型的错误输入时以可预测的方式崩溃”。 - Tom

13
你可以测试断言在遇到错误输入时是否会中止。
测试框架Google Test有一个ASSERT_DEATH宏,可以测试程序是否在你期望的位置中止(就像断言一样)。
你还可以使用已定义NDEBUG(使用gcc的-DNDEBUG)的编译选项禁用单元测试中的断言。

7

不,单元测试是在开发过程中进行的。Asserts是运行时构造。

据我的经验,大多数情况下,在生产环境中关闭了asserts。但你应该始终进行测试。

CppUnit是一个很好的测试框架。它是C++的nUnit家族的一部分。


3

在任何情况下,断言都不应该失败。如果在测试中它们失败了,那么就表示存在逻辑错误。基本上,如果你的函数执行的是"assert(0)"而不是返回错误代码,那么这个函数应该被重新编写。如果中止程序是期望的行为,那么使用exit()是合适的,但不要使用assert()。

如果在你的测试中有一个断言失败了,那么代码就存在错误,必须进行更改。代码"assert(x)"应该被解释为"程序的逻辑要求x为真。在任何情况下都不能为假。"如果你有一个单元测试导致断言失败,那么这个语句显然是无效的,必须进行修改。


1
如果中止是期望的行为,也许你应该使用 abort(),但绝对不要使用 exit() - Tom
那是一个很好的观点。也许我应该说“如果终止是期望的行为”。 “Abort”有一个技术含义,我并不打算使用它。 - William Pursell
2
那么,你如何测试无效的输入参数呢?首先,你要进行断言,然后(对于生产代码)在出现无效的输入参数时从函数中返回一个错误。 - philk
1
@philk。你为它们进行测试。如果逻辑上可能存在无效参数,则不使用断言。毫无意义的是进行断言,然后再对其进行测试。 - William Pursell

1

0
假设您的代码已准备好在关闭断言时优雅地处理可能触发断言的情况,并且您想对这些情况进行单元测试以确保它们被优雅地处理,一种方法是在相关对象文件中禁用断言并编译测试可执行文件。
例如,假设您在“Foo.c”中有以下函数:
Foo* bar(int const i)
{
    assert(i != 0);
    if (i == 0)
    {
        /*
         * When asserts are disabled, this returns `NULL`.
         * 
         * The code below can safely assume that `i != 0` --
         * not just because there's an assert up there, but
         * because this early return gracefully handles the
         * case where `i` is 0 when asserts are disabled.
         */
        return NULL;
    }
    
    // Do something with `i` here and return a non-`NULL` pointer.
}

您想测试当使用0调用bar()时,是否实际返回NULL

在您的makefile中,假设您有两个可执行文件:

  1. ${MAIN_EXE},您常规的可执行文件,在其中编写应用程序,并且您希望启用Foo.c的断言
  2. ${TESTS_EXE},运行单元测试的可执行文件,在其中禁用Foo.c的断言

Foo.c中,您还可以生成两个不同的目标文件

  1. Foo.o,启用断言的目标文件,将链接到${MAIN_EXE}
  2. Foo_test.o,禁用断言的目标文件,将链接到${TESTS_EXE}
# Main executable recipes
${MAIN_EXE}: main.o Foo.o
    ${CC} ${CFLAGS} main.o Foo.o -o ${MAIN_EXE}

Foo.o: Foo.c Foo.h
    ${CC} ${CFLAGS} -c Foo.c -o Foo.o

# ...

# Unit test executable recipes
${TESTS_EXE}: tests.o Foo_test.o
    ${CC} ${CFLAGS} tests.o Foo_test.o -o ${TESTS_EXE}

# For the test object, disable `Foo.c`'s asserts with `-DNDEBUG`.
Foo_test.o: Foo.c Foo.h
    ${CC} ${CFLAGS} -DNDEBUG -c Foo.c -o Foo_test.o

现在,运行测试可执行文件时,仅启用测试可执行文件的断言,使您能够像这样对bar()进行单元测试:
Foo* f = NULL;

f = bar(0); // Won't trigger the assert in `Foo.c`!

assert(f == NULL);

f = bar(5);

assert(f != NULL);

当运行您的常规可执行文件时,Foo.c的断言仍将像往常一样启用,以警告您代码中的错误。


1
以下代码并不假设 i != 0。那么如果断言被错误使用,就毫无意义了。断言用于编写契约式程序,我们断言我们所知道的是真实的,因为合同要求如此。 - StoryTeller - Unslander Monica
我表达不当,你引用的代码注释。"以下代码"实际上必须假定i != 0,因为它位于一个返回语句下面,如果i0,则返回NULL。我的意思是,如果没有在i == 0时早期返回,那么下面的代码将假定i != 0 只是因为有一个断言。这很糟糕,因为如果禁用断言,那个断言就永远不会被执行,而假设可能会突然变得错误。我已经重新表达了它,希望更清晰一些,从"The code below can safely assume that `i != 0` --"这一行开始。 - 6equj5

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接