C++单元测试。如何测试私有成员?

63

我想为我的C++应用程序编写单元测试。

如何正确地测试类的私有成员?是创建一个友元类来测试私有成员,使用派生类,还是其他技巧?

测试API使用哪种技术?


12
使用单元测试来测试接口的行为,因此您不应关心对象的内部状态。 - zerkms
10
很遗憾我们无法对评论进行踩。@BeniBela,我希望你能意识到你的建议是非常糟糕的编码实践。虽然挺有趣的。 - Steven Lu
7
我同意unittest是用来测试“合同”的。然而,您的代码中可能有某些部分受到(内部)“合同”的管辖,而您不希望将其暴露给代码用户。公共和私有主要是为了访问控制代码的消费者,而不一定是为了区分受合同管辖和不受管辖的部分。 - jdm
1
值得一提的是,gcc和clang都提供了“-fno-access-control”标志来禁用访问控制。看起来比宏要更简洁一些。 - Rusty Shackleford
2
@zerkms:单元测试不应该仅限于测试公共接口。私有方法用于实现公共接口的功能细节。不测试它们是不能被原谅的。毕竟,它被称为单元测试(而不是接口测试)是有原因的。 - Sampath
显示剩余9条评论
10个回答

59

通常,只有公共接口才会被测试,如问题的评论中所述。

然而,在某些情况下,测试私有或受保护方法会很有帮助。例如,实现中可能存在一些对用户隐藏的非平凡复杂性,并且可以通过访问非公共成员来更精确地测试。通常最好找出一种消除该复杂性或找出如何公开相关部分的方法,但不总是如此。

一种允许单元测试访问非公共成员的方法是使用friend构造。


35
终于有一个回答没有说“不要测试私有方法”,但即使技术差的工程师认为可以,包含关系并不总是有效。 - Aaron
36
我对这个网站上大量明确表示编写未经直接测试的代码(即私有成员)不仅可以,而且是可取的人感到困惑。有时候无法避免需要使用复杂的私有函数,有时候通过仅调用公共接口测试所有边缘情况是困难的。单元测试的真正目的是使代码变得更好,而不是区分公共和私有接口之类的无聊事情。在这些情况下,不测试私有成员是不专业的。 - Craig M. Brandenburg
5
在某些领域,例如安全关键的工业设备和医疗器械开发中,法规要求对私有和受保护的成员函数进行广泛测试...尽管从功能角度来看这样做是否合理并不重要。 - thinwybk

34

回答这个问题涉及到许多其他主题。除了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方法。


关于“永远不要测试私有事物”的讨论,我想补充一些内容。单元测试的好处之一就是,你很早就能发现需要改进代码设计的地方。这有时也是单元测试的缺点之一。它使得面向对象有时比必要的复杂。特别是如果你按照真实世界中的对象来设计类的规则,那么你有时需要把代码变得很丑陋,因为单元测试会强制你这样做。例如,处理控制物理过程的复杂框架就是一个例子。在那里,你想把代码映射到物理过程上,因为过程的某些部分已经非常复杂了。该过程上的依赖列表有时会变得非常长。这是一个可能出现的情况,测试私有成员变得美好的时刻。你必须权衡每种方法的优缺点。

类有时会变得很复杂!那么你必须决定将它们拆分或按原样使用。有时候第二个决定更有意义。最终,这总是一个关于你想达成的目标的问题(例如完美的设计、快速合并时间、低开发成本等)。


我的观点

我的访问私有成员的决策过程如下:

  1. 您需要测试私有成员吗?(通常会减少所需的总测试数量)
  2. 如果需要,您是否看到重构类的任何设计优势?
  3. 如果不需要,请在您的类中befriend测试(由于缺少替代方案而使用此方法)。

我不喜欢befriending方法,因为它会改变被测试的代码,但测试可能与交付的代码不同的风险不能证明更干净的代码是合理的。

顺便说一句:仅测试公共接口也是一种流畅的事情,因为根据我的经验,它的变化和私有实现一样频繁。因此,你没有减少对公共成员测试的优势。


1
“#define private public” 不是解决方案,因为它可能会改变类的布局,并使其与您尝试测试的代码不兼容。正式来说,这被称为违反了一个定义规则(One Definition Rule)。 (您测试编译器输出,而不仅仅是源代码,对吧?即使只测试源代码,现在您也没有测试相同的源代码。) - Ben Voigt
我并不担心ODR,改变顺序更加重要。我认为每个人都应该知道这一点,他们才会推荐使用#define private public。我喜欢C++,因为即使在使用10年后,你仍然能够学到一些新的东西。谢谢。 - Stefan Weiser
1
你真的应该删除关于“优点是它不会有侵入性,因此对测试代码无影响”的部分,因为这是非常错误的。编辑答案以确保准确性,这有什么不对吗? - xaxxon
我显然忽略了那个。所以我删掉了那句话。谢谢。 - Stefan Weiser
1
有一种方法可以简单地访问私有成员,而不会侵入或修改代码,如此处所述:http://bloglitb.blogspot.com.es/2010/07/access-to-private-members-thats-easy.html - Isaac Pascual
除了布局问题之外,“#define private public”也使其与OMP不兼容... - HerpDerpington

27

我自己没有找到完美的解决方案,但是你可以使用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的黑盒测试就不足够了。此外,如果遇到错误并编写额外的测试以防止回归,可能需要测试私有内容。


5
我知道这已经是老话题了,但我刚刚在使用gtest时尝试调用私有成员,并想要分享一下gtest对此的建议更新:测试私有成员。最小化地说,你现在可以使用FRIEND_TEST(TestCaseName, TestName)声明一个测试为友元,而不是依赖于命名约定。这使得它对生产环境更加安全!(需要包含 #include "gtest/gtest_prod.h")。 - define cindy const
1
之前评论的更新链接现在在这里:测试私有代码 - blacktide

4
有时需要测试私有方法。可以通过向类添加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()
}

为了让更多人了解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_

1
不要忘记在生产代码中添加这个头文件 #include "gtest/gtest_prod.h" - Syam Sanal
https://github.com/google/googletest/blob/master/googletest/include/gtest/gtest_prod.h - Syam Sanal

4
测试私有成员的欲望是一种设计上的缺陷,通常表明在您的类中有一个被困住想要脱身的类。一个类的所有功能都应该通过其公共方法来执行;无法公开访问的功能实际上并不存在。
有几种方法可以意识到您需要测试私有方法是否按照预期运行。其中最糟糕的是友元类;它们将测试与受测试的类的实现方式紧密联系在一起,这在本质上是不稳定的。稍微好一些的是依赖注入:使私有方法的依赖关系成为类属性,以便测试可以提供模拟版本,从而通过公共接口测试私有方法。最好的方法是提取一个封装了私有方法行为的类作为其公共接口,然后像平常一样测试新类。
有关更多详细信息,请参阅 Clean Code

一个只有Add()方法的接口怎么办呢? 你如何测试是否已经完成了加法运算?或者是否按照预期逻辑/回退行为进行了操作? - user3063349
如果唯一的接口是一个“Add”方法,那么如果它不起作用,没有人会知道,因此您不需要测试它。此外,您也不需要该方法或该类,并且可以享受删除一堆死代码的独特乐趣。 - darch
链接到“Clean Code”只会让这个答案变得更糟。 - Big Temp

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;
}

它是否比定义测试朋友更好?与备选方案相比,它似乎不那么冗长,并且在标题中清楚地说明了正在发生的事情。这两种解决方案都与安全无关:如果您真的关心方法或成员,则这些需要隐藏在可能带有其他保护措施的不透明实现内部。


1

在C++中有一个简单的解决方案,可以使用#define。只需像这样包装您的“ClassUnderTest”的包含:

#define protected public
#define private   public

#include <ClassUnderTest.hpp>

#undef protected
#undef private

[本文及RonFox提供了有价值的参考][1]


0
另一种解决方案是将 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,但如果您想将“测试”代码从生产类中排除,这是另一个选择。

0
我认为一个更好的解决方案是实现一个代码生成器。当提供一个类名或者一个包含需要测试的类的目录路径时,它应该扫描源代码,识别私有数据,并生成一个新的临时目录来存放生成的源代码。测试应该使用这个类或者路径来执行测试。
如果可能的话,它必须保留成员布局;否则测试将会是多余的。
这里的优点是没有对源代码本身进行干扰,而且生成的目录可以安全地被版本控制忽略。不需要反射。
缺点是这个目录的大小与输入源代码相同或更大,所以需要高覆盖率的大型项目可能会觉得它很繁琐。
那么在这些新文件中我们该做什么呢?
  1. 将测试类作为友元插入。测试将完全访问类的内部。可能会影响数据布局。
  2. 将私有成员变量改为公共。与上述相同,但可能会影响成员数据布局。
  3. 为该类插入一些友元或成员访问器(我不确定是否应该创建修改器,除非进行压力测试)。测试只能访问可见的内容,在像C++这样不对成员逐个进行访问限定的语言中,这允许以精细的方式暴露数据,而不必干扰数据布局。

最终,我们将得到类似于虚幻引擎的头文件工具,它将自定义宏类似的元数据解析为模板或生成的代码,并将其添加到项目中。


-4

我更喜欢在单元测试的Makefile中添加-Dprivate=public选项,而不是修改我的原始项目中的任何内容。


它能够工作,但肯定存在一些缺陷。 - Wizmann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接