关于c ++:测试用例VS ASSERTION语句

关于c ++:测试用例VS ASSERTION语句

Test Cases VS ASSERTION statement

在我的大多数C ++项目中,我大量使用ASSERTION语句,如下所示:

1
2
3
4
5
6
7
8
int doWonderfulThings(const int* fantasticData)
{
    ASSERT(fantasticData);
    if(!fantasticData)
        return -1;
    // ,,,
    return WOW_VALUE;
}

但是TDD社区似乎喜欢做这样的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
int doMoreWonderfulThings(const int* fantasticData)
{
    if(!fantasticData)
        return ERROR_VALUE;
    // ...
    return AHA_VALUE;
}

TEST(TDD_Enjoy)
{
    ASSERT_EQ(ERROR_VALUE, doMoreWonderfulThings(0L));
    ASSERT_EQ(AHA_VALUE, doMoreWonderfulThings("Foo"));
}

仅凭我的经验,我便消除了许多细微的错误。
但是TDD方法是处理遗留代码的非常聪明的主意。

"谷歌"(Google)-他们将"第一方法"与"背着救生衣走到岸上,没有任何安全保护措施游动海洋"进行比较。

哪一个更好?
哪一个使软件更强大?


以我(有限)的经验,第一个选择要安全得多。在一个测试用例中,您仅测试预定义的输入并比较结果,只要检查了每个可能的边缘用例,这就可以很好地工作。第一个选项仅检查每个输入,从而测试"实时"值,它可以快速真正过滤掉错误,但是会带来性能损失。

史蒂夫·麦康奈尔(Steve McConnell)在《代码完成》中向我们学习,第一种方法可以成功地用于滤除调试版本中的错误。在发行版构建中,您可以过滤掉所有断言(例如,带有编译器标志)以获取额外的性能。

我认为最好的方法是同时使用两种方法:

方法1捕获非法值

1
2
3
4
5
6
7
int doWonderfulThings(const int* fantasticData)
{
    ASSERT(fantasticData);
    ASSERTNOTEQUAL(0, fantasticData)

    return WOW_VALUE / fantasticData;
}

和方法2测试算法的边缘情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int doMoreWonderfulThings(const int fantasticNumber)
{
    int count = 100;
    for(int i = 0; i < fantasticNumber; ++i) {
        count += 10 * fantasticNumber;
    }
    return count;
}

TEST(TDD_Enjoy)
{
    // Test lower edge
    ASSERT_EQ(0, doMoreWonderfulThings(-1));
    ASSERT_EQ(0, doMoreWonderfulThings(0));
    ASSERT_EQ(110, doMoreWonderfulThings(1));

    //Test some random values
    ASSERT_EQ(350, doMoreWonderfulThings(5));
    ASSERT_EQ(2350, doMoreWonderfulThings(15));
    ASSERT_EQ(225100, doMoreWonderfulThings(150));
}

两种机制都有其价值。任何体面的测试框架无论如何都会捕获标准的assert(),因此导致断言失败的测试运行将导致测试失败。

我通常在每个c ++方法的开头都有一系列断言,并带有注释" // preconditions";这只是对我期望对象在调用该方法时所具有的状态的完整性检查。它们很好地与任何TDD框架吻合,因为它们不仅在您测试功能时在运行时起作用,而且在测试时也起作用。


您的测试包没有理由不能捕获诸如doMoreWonderfulThings中的断言。这可以通过让ASSERT处理程序支持回调机制来完成,或者您的测试断言包含try / catch块。


在C ++中,使用大多数测试框架时,我更喜欢方法2。通常,它使故障报告更容易理解。在编写测试后数月至数年的测试中,这是无价的。

我的理由是,大多数C ++测试框架都会在没有任何类型的堆栈跟踪信息的情况下打印出断言所在的文件和行号。因此,大多数情况下,您将在函数或方法内部而不是在测试用例内部获得报告行号。

即使断言被捕获并从调用者处重新断言,报告行也会与catch语句一起出现,并且可能不在靠近调用已断言的方法或函数的测试用例行的任何地方。当断言的功能可能在测试用例中多次使用时,这可能会很烦人。

但是也有例外。例如,Google的测试框架具有范围限定的跟踪语句,如果发生异常,该语句将作为跟踪的一部分打印。因此,您可以使用跟踪范围包装对通用测试函数的调用,并在一两行之内轻松告诉确切测试用例中的哪一行失败。


我不知道您指的是哪个特定的TDD子社区,但是我遇到的TDD模式要么使用Assert.AreEqual()获得肯定的结果,要么使用ExpectedException机制(例如.NET中的属性)来声明应该观察到的错误。


推荐阅读