我想为我的C ++应用程序进行单元测试。
测试班级私人成员的正确形式是什么?创建一个测试私有成员,使用派生类或其他技巧的朋友类?
测试API使用哪种技术?
答案 0 :(得分:37)
通常,只会测试公共界面,如问题评论中所述。
但有时候测试私有或受保护的方法会有所帮助。例如,实现可能具有一些对用户隐藏的非平凡复杂性,并且可以通过访问非公共成员来更精确地测试。通常最好找出一种方法来消除这种复杂性,或者弄清楚如何公开披露相关部分,但并非总是如此。
允许单元测试访问非公共成员的一种方法是通过friend构造。
答案 1 :(得分:22)
回答这个问题涉及许多其他主题。除了CleanCode,TDD和其他人的任何宗教信仰之外:
有多种方法可以访问私人会员。无论如何,你必须否决测试的代码!这在解析C ++(预处理器和语言本身)的两个级别都是可能的:
将所有内容定义为公开
通过使用预处理器,您可以打破封装。
#define private public
#define protected public
#define class struct
缺点是,交付代码的类与测试中的类不一样! 第9.2.13章中的C ++标准说:
具有不同的非静态数据成员的分配顺序 访问控制未指定。
这意味着编译器有权为测试重新排序成员变量和虚函数。如果没有缓冲区溢出发生,这可能会对您的类造成损害,但这意味着您不会测试与传递相同的代码。这意味着,如果您访问的对象成员是由代码初始化的,而private
未编译为public
,则您的成员的偏移量可能会有所不同!
<强>友强>
此方法需要更改测试类,以便使用测试类或测试函数与其进行交友。像gtest(FRIEND_TEST(..);
)这样的测试框架具有支持这种访问私有事物的特殊功能。
class X
{
private:
friend class Test_X;
};
它仅为测试打开类,不会打开世界,但您必须修改传递的代码。在我看来,这是一件坏事,因为测试永远不会改变测试代码。作为另一个缺点,它为交付代码的其他类提供了通过将自己命名为测试类来侵入您的类的可能性(这也会损害C ++标准的ODR规则)。
声明受保护的私人物品并从测试类派生
不是一种非常优雅的方式,非常具有侵入性,但也有效:
class X
{
protected:
int myPrivate;
};
class Test_X: public X
{
// Now you can access the myPrivate member.
};
使用宏的任何其他方法
可行,但在标准符合性方面具有与第一种方式相同的缺点。 e.g:
class X
{
#ifndef UNITTEST
private:
#endif
};
我认为最后两种方式都不能替代前两种方式,因为它们没有第一种方式的优势,但对测试代码更具侵入性。第一种方式风险很大,所以你可以使用友好的方法。
关于永不测试私人事物讨论的一些话。单元测试的一个优点是,您将很早就达到这一点,您必须改进代码的设计。这有时也是单元测试的缺点之一。它使得面向对象有时比它必须更加复杂。特别是如果你按照规则设计类,就像现实世界的对象一样。
然后你必须将代码更改为丑陋的东西,因为单元测试方法迫使你这样做。处理用于控制物理过程的复杂框架就是一个例子。在那里,您希望在物理过程中映射代码,因为过程的某些部分通常非常复杂。该进程的依赖列表有时会很长。这是一个可能的时刻,测试私人成员变得越来越好。你必须权衡每种方法的优点和缺点。
课程有时变得复杂!然后你必须决定拆分它们或按原样取下它们。有时第二个决定更有意义。最后,您总是要想实现哪些目标(例如完美的设计,快速的合并时间,低开发成本......)。
我的意见
我访问私人会员的决策过程如下:
我不喜欢这种友好的方法,因为它改变了经过测试的代码,但是测试某些内容的风险(可能与第一种方法不同)可能无法证明清洁代码的合理性。
BTW:仅测试公共接口也是一个流畅的问题,因为根据我的经验,它会像私有实现一样经常更改。因此,减少对公共成员的测试没有任何优势。答案 2 :(得分:19)
我自己没有找到一个黄金解决方案,但如果您知道测试框架如何命名它的方法,您可以使用friend
来测试私有成员。我使用以下内容通过Google测试来测试私人成员。虽然这很有效,但请注意它是一个黑客,我不会在生产代码中使用它。
在我要测试的代码(stylesheet.h)的标题中,我有:
#ifndef TEST_FRIENDS
#define TEST_FRIENDS
#endif
class Stylesheet {
TEST_FRIENDS;
public:
// ...
private:
// ...
};
在测试中我有:
#include <gtest/gtest.h>
#define TEST_FRIENDS \
friend class StylesheetTest_ParseSingleClause_Test; \
friend class StylesheetTest_ParseMultipleClauses_Test;
#include "stylesheet.h"
TEST(StylesheetTest, ParseSingleClause) {
// can use private members of class Stylesheet here.
}
如果添加访问私有成员的新测试,则总是向TEST_FRIENDS添加新行。这种技术的好处在于它在测试代码中相当不引人注目,因为你只添加了几个#defines,它们在不测试时没有效果。缺点是测试中有点冗长。
现在可以说一下你为什么要这样做。当然,理想情况下,您可以使用定义明确的小类,并且类具有易于测试的接口。然而,在实践中并不总是那么容易。如果您正在编写库,那么private
和public
取决于您希望库的使用者能够使用的内容(您的公共API),而不是需要测试的内容或不。您可以使用不太可能更改的不变量,并且需要进行测试,但API的使用者不感兴趣。然后,API的黑盒测试是不够的。此外,如果您遇到错误并编写其他测试以防止回归,则可能需要测试private
内容。
答案 3 :(得分:4)
测试私人成员的愿望是一种设计气味,通常表明你的班级中有一个阶级难以逃脱。一个类的所有功能都应该通过其公共方法来行使;无法公开访问的功能实际上并不存在。
有几种方法可以实现您需要测试您的私有方法是否能够执行他们所说的内容。朋友班是最糟糕的;他们以一种看似脆弱的方式将测试与测试中的类的实现联系起来。更好的是依赖注入:使私有方法的依赖类属性,测试可以提供模拟版本,以允许通过公共接口测试私有方法。最好的方法是提取一个封装了私有方法作为公共接口的行为的类,然后像往常一样测试新类。
有关详细信息,请参阅Clean Code。
答案 4 :(得分:3)
尽管有关于测试私有方法的适当性的评论,但假设您确实需要......例如,在将遗留代码重构为更合适的代码之前,通常会这样做。这是我用过的模式:
// In testable.hpp:
#if defined UNIT_TESTING
# define ACCESSIBLE_FROM_TESTS : public
# define CONCRETE virtual
#else
# define ACCESSIBLE_FROM_TESTS
# define CONCRETE
#endif
然后,在代码中:
#include "testable.hpp"
class MyClass {
...
private ACCESSIBLE_FROM_TESTS:
int someTestablePrivateMethod(int param);
private:
// Stuff we don't want the unit tests to see...
int someNonTestablePrivateMethod();
class Impl;
boost::scoped_ptr<Impl> _impl;
}
这比定义测试朋友更好吗?它似乎比替代方案更简洁,并且在标题内清楚地显示正在发生的事情。这两种解决方案都与安全无关:如果您真的关心方法或成员,那么这些需要隐藏在不透明的实现中,可能还有其他保护措施。
答案 5 :(得分:1)
有时,需要测试私有方法。可以通过将FRIEND_TEST添加到类中来进行测试。
// Production code
// prod.h
#include "gtest/gtest_prod.h"
...
class ProdCode
{
private:
FRIEND_TEST(ProdTest, IsFooReturnZero);
int Foo(void* x);
};
//Test.cpp
// TestCode
...
TEST(ProdTest, IsFooReturnZero)
{
ProdCode ProdObj;
EXPECT_EQ(0, ProdObj.Foo(NULL)); //Testing private member function Foo()
}
答案 6 :(得分:-1)
使用#define在C ++中有一个简单的解决方案。只需包装你的“ClassUnderTest”包括:
#define protected public
#define private public
#include <ClassUnderTest.hpp>
#undef protected
#undef private
[读到这篇文章和RonFox] [1]
答案 7 :(得分:-2)
我更希望在单元测试的Makefile中添加-Dprivate = public选项,避免修改原始项目中的任何内容