今天我和一位同事讨论了在课堂上是否测试私有成员或私有状态。他几乎说服了我为什么这样做是有道理的。这个问题的目的不是重复已经存在的关于测试私有成员的性质和原因的 StackOverflow 问题,例如:让单元测试成为它正在测试的类的朋友有什么问题?
同事的建议在我看来有点脆弱,将朋友声明引入单元测试实现类。在我看来这是不行的,因为我们在测试代码中引入了一些测试代码的依赖,而测试代码已经依赖于测试代码 => 循环依赖。即使是像重命名测试类这样无辜的事情也会导致破坏单元测试并在测试代码中强制执行代码更改。
我想请 C++ 大师来判断另一个建议,它依赖于我们被允许专门化模板函数的事实。想象一下这个类:
// tested_class.h
struct tested_class
{
tested_class(int i) : i_(i) {}
//some function which do complex things with i
// and sometimes return a result
private:
int i_;
};
我不喜欢为 i_ 设置吸气剂只是为了使其可测试的想法。所以我的建议是类中的'test_backdoor'函数模板声明:
// tested_class.h
struct tested_class
{
explicit
tested_class(int i=0) : i_(i) {}
template<class Ctx>
static void test_backdoor(Ctx& ctx);
//some function which do complex things with i
// and sometimes return a result
private:
int i_;
};
通过添加这个函数,我们可以使类的私有成员可测试。请注意,不依赖于单元测试类,也不依赖于模板函数实现。在此示例中,单元测试实现使用 Boost Test 框架。
// tested_class_test.cpp
namespace
{
struct ctor_test_context
{
tested_class& tc_;
int expected_i;
};
}
// specialize the template member to do the rest of the test
template<>
void tested_class::test_backdoor<ctor_test_context>(ctor_test_context& ctx)
{
BOOST_REQUIRE_EQUAL(ctx.expected_i, tc_.i_);
}
BOOST_AUTO_TEST_CASE(tested_class_default_ctor)
{
tested_class tc;
ctor_test_context ctx = { tc, 0 };
tested_class::test_backdoor(ctx);
}
BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
tested_class tc(-5);
ctor_test_context ctx = { tc, -5 };
tested_class::test_backdoor(ctx);
}
通过只引入一个完全不可调用的模板声明,我们为测试实现者提供了将测试逻辑转发到函数中的可能性。由于测试上下文的匿名类型性质,该函数作用于类型安全上下文并且仅在特定测试编译单元内部可见。最好的事情是,我们可以定义任意数量的匿名测试上下文并对它们进行专门测试,而无需接触被测试的类。
当然,用户必须知道模板专业化是什么,但这段代码真的很糟糕、奇怪或不可读吗?或者我是否可以期望 C++ 开发人员了解 C++ 模板专业化是什么以及它是如何工作的?
详细说明使用朋友声明单元测试类我不认为这是健壮的。想象一下 boost 框架(或者可能是其他测试框架)。它为每个测试用例生成一个单独的类型。但我为什么要关心,只要我能写:
BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
...
}
如果使用朋友,我必须将每个测试用例声明为朋友......或者最终以某种常见类型(如夹具)引入一些测试功能,将其声明为朋友,并将所有测试调用转发到该类型...... . 这不是很奇怪吗?
我想看看你练习这种方法的利弊。