TDD:在C ++中给定未定义的行为,确定性地测试成员初始化

时间:2014-02-13 09:35:30

标签: c++ unit-testing tdd undefined-behavior googletest

注意:我知道active_在我的例子中可能是“任何东西”。这不是这个问题的内容。它是关于使“未定义的值”可靠地通过单元测试失败。

编辑:从“无构造函数”更改为“空构造函数”。

我正在开发一个C ++类,我正在使用TDD。现在我想确保正确初始化bool类成员 - 在构造函数中为其分配值。所以我编写了以下测试(使用Google Mock / Google Test框架):

TEST(MyClass, isNotActiveUponCreation) {
    MyClass my;
    ASSERT_FALSE(my.isActive());
}

以下类定义:

class MyClass {
public:
    // Note: Constructor doesn't initialize active_
    MyClass() {}

    bool isActive() const { return active_; }
private:
    bool active_;
};

问题:在我的机器上,即使active_从未初始化,此测试也会始终通过。现在我们知道active_的值是未定义的,因为它是一个原始类型并且从未初始化。所以从理论上讲,它可能在某些时候true,但最终,不可能知道。最重要的是,我无法使用这种方法可靠地测试缺少的初始化。

有没有人知道如何以确定性和可重复的方式测试这种情况?或者我必须忍受它,省略这种测试,并希望我永远不会忘记初始化一个布尔成员,或者其他测试将始终捕获产生的缺陷?

6 个答案:

答案 0 :(得分:3)

在阅读了TobiMcNamobi的回答后,我记得placement new,并了解了如何解决我的问题。除非我在构造函数中初始化active_,否则以下测试可靠地失败:

#include <gmock/gmock.h>
#include <vector>

class MyClass {
public:
    // Note: Constructor doesn't initialize active_
    MyClass() {}

    bool isActive() const { return active_; }
private:
    bool active_;
};

TEST(MyClass, isNotActiveUponCreation) {
    // Memory with well-known content
    std::vector<char> preFilledMemory(sizeof(MyClass), 1);

    // Create a MyClass object in that memory area using placement new
    auto* myObject = new(preFilledMemory.data()) MyClass();


    ASSERT_FALSE(myObject->isActive());
    myObject->~MyClass();
}

现在我承认这个测试不是最可读的,并且可能不会立即清楚,但它可靠地运行并独立于任何第三方工具,如valgrind。是否值得额外的努力?我不确定。它在很大程度上取决于MyClass内部,这将使它非常脆弱。无论如何,它是在C ++中测试正确初始化对象的一种方法。

答案 1 :(得分:1)

一旦你进行了单元测试,这类问题实际上非常容易进行单元测试。

只需在内存检查器下运行单元测试(linux上的valgrind,不确定Windows上使用的是什么)。

我没有创建gtest可执行文件,而是创建了一个简单的例子:

#include <iostream>

class MyClass {
public:
    // Note: no constructor

    bool isActive() const { return active_; }
private:
    bool active_;
};

int main()
{
    MyClass c;  // line 17

    std::cout << c.isActive() << std::endl;
}

在valgrind下运行它,我得到了下一个输出(修剪不需要的行):

==9217== 
==9217== Conditional jump or move depends on uninitialised value(s)
.....
==9217==    by 0x40094F: main (garbage.cpp:17)

使用valgrind执行单元测试时,您将遇到与内存访问相关的所有问题。你也会得到回溯。

答案 2 :(得分:1)

我的小测试:

#include <stdlib.h>
#include <vector>
#include <iostream>

class MyClass {
public:
    // Note: Constructor doesn't initialize active_
    MyClass() {}

    bool isActive() const { return active_; }
private:
    bool active_;
};

int main(int argc, char* argv[])
{
    std::vector<MyClass> vec(1000);
    for (int i = 0; i < 1000; i++)
    {
        std::cout << (vec[i].isActive() ? "1" : "0");
    }

    return system("pause");
}

那么执行此操作时会发生什么(使用VS2012编译)?

调试配置:写入1千字节。

发布配置:写入0 0。我把迭代次数提高到了10万,并得到了十万0 ...等等,前几个数字是10101110000000 ......那里!在这样的另一个序列之间的某处是隐藏的。

这是什么意思?结果或多或少是预期的。您无法预测单个未初始化位的设置方式。你想在这里做的是初始化内存中的一些空间并在那里创建一个对象,就好像那个内存之前没有初始化一样。

所以,直到我被证明是错误的:你无法对它进行单元测试

除非您使用工具(例如valgrind,请参阅其他答案)。

我认为TDD很棒,但它当然有其局限性。我只是初始化标志并继续红绿重构循环。

答案 3 :(得分:1)

TDD测试应该行使行为而不是实施细节。构造函数初始化,setter初始化等的测试取决于具体的实现,如果实现被重构,则会很脆弱。

答案 4 :(得分:0)

你要做的是对未定义的行为进行单元测试,这是毫无意义的,因为显然需要接受所有结果。

答案 5 :(得分:0)

如果使用MemorySanitizer编译单元测试套件,那么从未初始化的内存中读取任何内容都会导致测试失败。