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

TDD: Test member initialization deterministically, given undefined behavior in C++

本文关键字:C++ 给定 未定义 初始化 成员 测试 TDD      更新时间:2023-10-16

注意:我知道active_在我的例子中可以是"任何东西"。这不是这个问题的意义所在。这是关于让一个"未定义的值"可靠地通过单元测试。

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

我正在开发一个C++类,并且正在使用TDD。现在,我想确保bool类成员被正确初始化——在构造函数中为其赋值。因此,我编写了以下测试(使用谷歌模拟/谷歌测试框架):

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,但最终,不可能知道。最重要的是,我无法使用这种方法可靠地测试是否缺少初始化。

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

在阅读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++中测试正确初始化对象的一种方法。。

一旦有了单元测试,这种问题实际上很容易进行单元测试。

只需在内存检查器下运行单元测试(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执行单元测试时,你会遇到各种与内存访问有关的问题。您还将获得回溯。

我的小测试:

#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。我把迭代次数提高到100000,得到了十万个0。。。等等,前几个数字是10101110000000……然后就到了!像这样的另一个序列之间的某个地方是隐藏的。

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

因此,在我被证明是错误的之前:您不能对其进行单元测试

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

我认为TDD很棒,但它当然也有局限性。我只需要初始化标志,然后继续红-绿重构循环。

TDD测试应该练习行为而不是实现细节。构造函数初始化、setter初始化等的测试取决于特定的实现,如果重构该实现,这些测试将很脆弱。

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

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