我想为我的C++应用程序编写单元测试。
如何正确地测试类的私有成员?是创建一个友元类来测试私有成员,使用派生类,还是其他技巧?
测试API使用哪种技术?
我想为我的C++应用程序编写单元测试。
如何正确地测试类的私有成员?是创建一个友元类来测试私有成员,使用派生类,还是其他技巧?
测试API使用哪种技术?
通常,只有公共接口才会被测试,如问题的评论中所述。
然而,在某些情况下,测试私有或受保护方法会很有帮助。例如,实现中可能存在一些对用户隐藏的非平凡复杂性,并且可以通过访问非公共成员来更精确地测试。通常最好找出一种消除该复杂性或找出如何公开相关部分的方法,但不总是如此。
一种允许单元测试访问非公共成员的方法是使用friend构造。
回答这个问题涉及到许多其他主题。除了CleanCode、TDD等方面的任何信仰之外:
有几种访问私有成员的方法。在任何情况下,您都必须覆盖已测试的代码!这在解析C++的两个级别(预处理器和语言本身)上都是可能的:
将所有内容定义为公共
通过使用预处理器,您可以打破封装。
#define private public
#define protected public
#define class struct
缺点在于,交付代码的类与测试中的类不同!
C ++标准第9.2.13章节中写道:
这意味着编译器有权为测试重新排序成员变量和虚函数。你可能会认为,如果没有缓冲区溢出,这不会损害你的类,但它意味着你无法测试与你交付的相同的代码。这意味着,如果你访问由使用private而非定义为public编译的代码初始化的对象的成员,则你的成员偏移量可能会有所不同! 友元 这种方法需要更改被测类,以使其成为测试类或测试函数的友元。一些测试框架(如gtest)具有支持此访问私有成员的特殊功能。具有不同访问控制的非静态数据成员的分配顺序是未指定的。
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.
};
使用宏的其他方法
虽然这种方法是可行的,但它和第一种方法一样,也存在标准符合性的缺点。例如:
class X
{
#ifndef UNITTEST
private:
#endif
};
我认为最后的两种方法都不是第一种方法的替代品,因为它们与第一种方法相比没有任何优点,但是对被测试代码的干扰更大。第一种方法非常冒险,所以您可以使用befriending方法。
关于“永远不要测试私有事物”的讨论,我想补充一些内容。单元测试的好处之一就是,你很早就能发现需要改进代码设计的地方。这有时也是单元测试的缺点之一。它使得面向对象有时比必要的复杂。特别是如果你按照真实世界中的对象来设计类的规则,那么你有时需要把代码变得很丑陋,因为单元测试会强制你这样做。例如,处理控制物理过程的复杂框架就是一个例子。在那里,你想把代码映射到物理过程上,因为过程的某些部分已经非常复杂了。该过程上的依赖列表有时会变得非常长。这是一个可能出现的情况,测试私有成员变得美好的时刻。你必须权衡每种方法的优缺点。
类有时会变得很复杂!那么你必须决定将它们拆分或按原样使用。有时候第二个决定更有意义。最终,这总是一个关于你想达成的目标的问题(例如完美的设计、快速合并时间、低开发成本等)。
我的观点
我的访问私有成员的决策过程如下:
我不喜欢befriending方法,因为它会改变被测试的代码,但测试可能与交付的代码不同的风险不能证明更干净的代码是合理的。
顺便说一句:仅测试公共接口也是一种流畅的事情,因为根据我的经验,它的变化和私有实现一样频繁。因此,你没有减少对公共成员测试的优势。
#define private public
。我喜欢C++,因为即使在使用10年后,你仍然能够学到一些新的东西。谢谢。 - Stefan Weiser我自己没有找到完美的解决方案,但是你可以使用friend
来测试私有成员,如果你知道测试框架如何命名它的方法。我使用以下代码在 Google test 中测试私有成员。虽然这个方法非常有效,但请注意,这是一个hack,并且我不会在生产代码中使用它。
在我想要测试的代码头文件 (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中。这种技术的好处在于,在测试代码中,您只需要添加一些没有任何影响的#define代码,因此对被测试的代码不会产生太大的干扰。缺点是测试用例会有点啰嗦。
现在,我们来解释一下为什么要这样做。理想情况下,您有小巧明确职责的类,并且这些类具有易于测试的接口。然而,在实践中,这并不总是容易的。如果您正在编写库,则private和public由您希望库的使用者能够使用的内容(公共API)来决定,而不是由需要进行测试的内容来决定。您可能会有非常不太可能改变但需要测试的不变量,而这些不变量对于API使用者来说并不重要。那么,对API的黑盒测试就不足够了。此外,如果遇到错误并编写额外的测试以防止回归,可能需要测试私有内容。
// 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()
}
为了让更多人了解gtest的功能特性,我希望你能添加更多信息。
以下内容来自于gtest/gtest_prod.h
:
// Copyright 2006, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Google C++ Testing and Mocking Framework definitions useful in production code.
// GOOGLETEST_CM0003 DO NOT DELETE
#ifndef GTEST_INCLUDE_GTEST_GTEST_PROD_H_
#define GTEST_INCLUDE_GTEST_GTEST_PROD_H_
// When you need to test the private or protected members of a class,
// use the FRIEND_TEST macro to declare your tests as friends of the
// class. For example:
//
// class MyClass {
// private:
// void PrivateMethod();
// FRIEND_TEST(MyClassTest, PrivateMethodWorks);
// };
//
// class MyClassTest : public testing::Test {
// // ...
// };
//
// TEST_F(MyClassTest, PrivateMethodWorks) {
// // Can call MyClass::PrivateMethod() here.
// }
//
// Note: The test class must be in the same namespace as the class being tested.
// For example, putting MyClassTest in an anonymous namespace will not work.
#define FRIEND_TEST(test_case_name, test_name)\
friend class test_case_name##_##test_name##_Test
#endif // GTEST_INCLUDE_GTEST_GTEST_PROD_H_
尽管有关测试私有方法是否适当的评论,但假设您确实需要这样做...例如,在将遗留代码重构为更合适的内容之前,经常会出现这种情况。以下是我使用的模式:
// 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;
}
它是否比定义测试朋友更好?与备选方案相比,它似乎不那么冗长,并且在标题中清楚地说明了正在发生的事情。这两种解决方案都与安全无关:如果您真的关心方法或成员,则这些需要隐藏在可能带有其他保护措施的不透明实现内部。
在C++中有一个简单的解决方案,可以使用#define
。只需像这样包装您的“ClassUnderTest”的包含:
#define protected public
#define private public
#include <ClassUnderTest.hpp>
#undef protected
#undef private
[本文及RonFox提供了有价值的参考][1]
private
成员标记为 protected
,并创建一个派生类来使用公共函数公开这些私有成员,而不需要将任何 gtest
标头或宏添加到您的生产代码中。#include <cstdint>
class Widget {
protected:
uint8_t foo;
uint8_t bar;
void baz();
}
还有测试类:
#include "gtest/gtest.h"
#include <cstdint>
#include "../src/widget.h"
// create a "test" class derived from your production class
class TestWidget : public Widget {
public:
// create public methods that expose the private members
uint8_t getFoo() {
return foo;
}
void callBar() {
bar();
}
}
TEST(WidgetTests, TestFoo) {
// Use the derived class in your tests
TestWidget widget;
// call a protected method
widget.callBar();
// check the value of a protected member
EXPECT_EQ(widget.getFoo(), 10);
}
private
成员标记为protected
,但如果您想将“测试”代码从生产类中排除,这是另一个选择。最终,我们将得到类似于虚幻引擎的头文件工具,它将自定义宏类似的元数据解析为模板或生成的代码,并将其添加到项目中。
我更喜欢在单元测试的Makefile中添加-Dprivate=public选项,而不是修改我的原始项目中的任何内容。