Testing private class member in C++ without friend
今天我和一位同事讨论了在课堂上是否要测试私人成员或私人国家。他几乎让我相信这是有道理的。这个问题并不打算复制已经存在的关于测试私有成员的性质和原因的stackoverflow问题,比如:让单元测试成为它正在测试的类的朋友有什么问题?
同事们的建议在我看来有点脆弱,可以将friend声明引入到单元测试实现类中。在我看来,这是不可行的,因为我们将测试代码的一些依赖性引入到测试代码中,而测试代码已经依赖于测试代码=>循环依赖性。即使是像重命名一个测试类这样的无辜的事情也会导致破坏单元测试并强制测试代码中的代码更改。
我想请C++ Guuru对另一个建议进行判断,这取决于我们允许专门化模板函数的事实。想象一下课堂:
1 2 3 4 5 6 7 8 9 10 11 12 | // 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_; }; |
我不喜欢这样一个想法,我有一个getter只是为了让它可测试。所以我的建议是类中的"test_backdoor"函数模板声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 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测试框架。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 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++模板专业化是什么以及它是如何工作的?
详细说明如何使用friend来声明单元测试类,我认为这并不健壮。想象一下Boost框架(或者可能是其他测试框架)。它为每个测试用例生成一个单独的类型。但我为什么要关心我能写的那么久:
1 2 3 4 | BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor) { ... } |
如果使用friends,我必须将每个测试用例声明为friends,然后…或者最后在一些公共类型(如fixture)中引入一些测试功能,将其声明为朋友,并将所有测试调用转发给该类型…这不奇怪吗?
我希望看到你的赞成和反对者练习这种方法。
我认为单元测试是测试被测类的可观察行为。因此,不需要测试私有部分,因为它们本身是不可观测的。测试它的方法是测试对象的行为是否如您所期望的那样(这意味着所有私有内部状态都是有序的)。
不关心私有部分的原因是,通过这种方式,您可以更改实现(例如重构),而不必重写测试。
所以我的答案是不要这样做(即使技术上可能),因为这违背了单元测试的原理。
赞成的意见
- 您可以访问私有成员来测试它们
- 它只相当少量的
hack
欺骗
- 破损的封装
- 破损的封装,更复杂,和
friend 一样易碎。 - 将
test_backdoor 放在生产侧,与生产代码混合测试 - 维护问题(就像加入测试代码一样,您已经创建了与测试代码的紧密耦合)
撇开所有的优点/缺点不谈,我认为您最好做一些架构更改,以便更好地测试正在发生的任何复杂的事情。
可能的解决方案
- 使用pimpl习惯用法,将
complex 代码和私有成员放在pimpl中,并为pimpl编写测试。pimpl可以被转发声明为公共成员,从而允许在单元测试中进行外部实例化。PIMPL只能由公共成员组成,这样更容易测试- 缺点:代码很多
- 缺点:不透明类型,调试时更难看到内部
- 只需测试类的公共/受保护接口。测试您的接口所设计的契约。
- 缺点:单元测试很难/不可能以独立的方式写入。
- 与PIMPL解决方案类似,但使用其中的
complex 代码创建一个自由函数。将声明放在私有头(不是库公共接口的一部分)中,然后测试它。 - 通过friend A测试方法/夹具破坏封装
- 可能的变化是:声明
friend struct test_context; ,将测试代码放在实现struct test_context 的方法中。这样,您就不必为每个测试用例、方法或夹具交朋友了。这会降低某人破坏友谊的可能性。
- 可能的变化是:声明
- 通过模板专门化中断封装
很抱歉,我建议这样做,但如果没有强有力的重构,这些答案中的大多数方法都无法实现,这对我有帮助:在文件头前面添加您希望访问其私有成员的类,
1 | #define private public |
它是邪恶的,但
不干扰生产代码
不会像朋友/更改访问级别那样破坏封装
避免使用pimpl习惯用法进行严重的重构
所以你可以去…
接下来要做的不是在技术上直接回答你问题是它仍然会利用"朋友"功能但它不需要修改被测试实体本身我认为它增加了破坏封装的问题在其他一些答案中提到;但它确实需要写一些样板代码。
它背后的想法不是我的,实现是完全基于一个由Litb提出并解释的技巧博客(加上这个萨特的哥特更多的背景,至少对我来说是这样)-简而言之,CRTP、朋友、ADL和指向成员的指针(我必须承认,令我沮丧的是,我仍然没有完全明白,但我正在做一个相关的100%计算。
我用GCC4.6、CLANG 3.1和VS2010编译器测试了它。工作得很好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | /* test_tag.h */ #ifndef TEST_TAG_H_INCLUDED_ #define TEST_TAG_H_INCLUDED_ template <typename Tag, typename Tag::type M> struct Rob { friend typename Tag::type get(Tag) { return M; } }; template <typename Tag, typename Member> struct TagBase { typedef Member type; friend type get(Tag); }; #endif /* TEST_TAG_H_INCLUDED_ */ /* tested_class.h */ #ifndef TESTED_CLASS_H_INCLUDED_ #define TESTED_CLASS_H_INCLUDED_ #include <string> struct tested_class { tested_class(int i, const char* descr) : i_(i), descr_(descr) { } private: int i_; std::string descr_; }; /* with or without the macros or even in a different file */ # ifdef TESTING_ENABLED # include"test_tag.h" struct tested_class_i : TagBase<tested_class_i, int tested_class::*> { }; struct tested_class_descr : TagBase<tested_class_descr, const std::string tested_class::*> { }; template struct Rob<tested_class_i, &tested_class::i_>; template struct Rob<tested_class_descr, &tested_class::descr_>; # endif #endif /* TESTED_CLASS_H_INCLUDED_ */ /* test_access.cpp */ #include"tested_class.h" #include <cstdlib> #include <iostream> #include <sstream> #define STRINGIZE0(text) #text #define STRINGIZE(text) STRINGIZE0(text) int assert_handler(const char* expr, const char* theFile, int theLine) { std::stringstream message; message <<"Assertion" << expr <<" failed in" << theFile <<" at line" << theLine; message <<"." << std::endl; std::cerr << message.str(); return 1; } #define ASSERT_HALT() exit(__LINE__) #define ASSERT_EQUALS(lhs, rhs) ((void)(!((lhs) == (rhs)) && assert_handler(STRINGIZE((lhs == rhs)), __FILE__, __LINE__) && (ASSERT_HALT(), 1))) int main() { tested_class foo(35,"Some foo!"); // the bind pointer to member by object reference could // be further wrapped in some"nice" macros std::cout <<" Class guts:" << foo.*get(tested_class_i()) <<" -" << foo.*get(tested_class_descr()) << std::endl; ASSERT_EQUALS(35, foo.*get(tested_class_i())); ASSERT_EQUALS("Some foo!", foo.*get(tested_class_descr())); ASSERT_EQUALS(80, foo.*get(tested_class_i())); return 0; } |
我想首先要问的是:为什么朋友被认为是必须谨慎使用的东西?
因为它破坏了封装。它提供了另一个类或函数来访问对象的内部,从而扩展了私有成员的可见范围。如果你有很多朋友,就很难解释你的物品的状态。
在我看来,模板解决方案在这方面甚至比朋友更糟糕。模板的主要优点是不再需要在类中显式地使测试成为朋友。我认为,恰恰相反,这是一种损害。这有两个原因。
测试耦合到类的内部。任何更改类的人都应该知道,通过更改对象的隐私,他们可能会破坏测试。friend确切地告诉他们哪些对象可能耦合到类的内部状态,但是模板解决方案没有。
朋友限制了你的私人空间。如果你是一个类的朋友,你知道只有那个类可以访问你的内部。因此,如果您是测试的朋友,您知道只有测试可以读写私有成员变量。但是,您的模板后门可以在任何地方使用。
模板解决方案无效,因为它隐藏了问题,而不是修复问题。循环依赖的基础问题仍然存在:更改类的人必须知道后门的每种使用,更改测试的人必须知道类。基本上,从类中删除对测试的引用只是通过以迂回的方式将所有私有数据转换为公共数据。
如果您必须从您的测试中访问私有成员,那么您只需与测试夹具交朋友就可以了。简单易懂。
我使用了一个函数来测试私有类成员,该成员被称为testinvariant()。
它是类的私有成员,在调试模式下,在每个函数的开始和结束时调用(ctor的开始和dctor的结束除外)。
它是虚拟的,任何基类在拥有父版本之前都会调用它。
这使得我可以随时验证类的内部状态,而不向任何人公开类的意图。我做了非常简单的测试,但没有理由不做复杂的测试,甚至用一个旗子来打开或关闭它。
此外,您还可以拥有公共测试函数,其他调用testinvariant()函数的类可以调用这些函数。因此,当需要更改内部类工作时,不需要更改任何用户代码。
这有帮助吗?
测试私有成员并不总是通过检查状态是否等于某些预期值来验证状态。为了适应其他更复杂的测试场景,我有时使用以下方法(在这里简化以传达主要思想):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // Public header struct IFoo { public: virtual ~IFoo() { } virtual void DoSomething() = 0; }; std::shared_ptr<IFoo> CreateFoo(); // Private test header struct IFooInternal : public IFoo { public: virtual ~IFooInternal() { } virtual void DoSomethingPrivate() = 0; }; // Implementation header class Foo : public IFooInternal { public: virtual DoSomething(); virtual void DoSomethingPrivate(); }; // Test code std::shared_ptr<IFooInternal> p = std::dynamic_pointer_cast<IFooInternal>(CreateFoo()); p->DoSomethingPrivate(); |
这种方法有明显的优势,可以促进良好的设计,而不会弄乱朋友的声明。当然,大多数时候您不必经历麻烦,因为从一开始就能够测试私有成员是非常不标准的要求。
我通常不觉得有必要对私有成员和函数进行单元测试。我可能更愿意引入一个公共函数来验证正确的内部状态。
但是,如果我真的决定去探究细节,我会在单元测试程序中使用一个讨厌的快速黑客程序:
1 2 3 4 5 6 7 8 9 | #include <system-header> #include <system-header> // Include ALL system headers that test-class-header might include. // Since this is an invasive unit test that is fiddling with internal detail // that it probably should not, this is not a hardship. #define private public #include"test-class-header.hpp" ... |
在Linux上,至少这是有效的,因为C++名称MINGLING不包括私有/公共状态。我被告知,在其他系统上,这可能不是真的,也不会链接。
有一种理论认为,如果它是私有的,就不应该单独测试,如果它需要这样做,那么就应该重新设计。
对我来说,这就是什叶派。
在某些项目中,人们为私有方法创建宏,就像:
1 2 3 4 | class Something{ PRIVATE: int m_attr; }; |
当编译测试private时定义为public,否则定义为private。很简单。