如何测试类的私有成员和方法?

49

我正试图在一个名为VariableImpl的C++类上进行单元测试(使用Boost单元测试框架)。以下是详细信息。

class Variable
{
public:
  void UpdateStatistics (void) {
    // compute mean based on m_val and update m_mean;
    OtherClass::SendData (m_mean);
    m_val.clear ();
  }
  virtual void RecordData (double) = 0;

protected:
  std::vector<double> m_val;

private:
  double m_mean;
};

class VariableImpl : public Variable
{
public:
  virtual void RecordData (double d) {
    // Put data in m_val
  }
};

我该如何检查平均值是否被正确计算?请注意:1)m_mean受保护,2)UpdateStatistics调用另一个类的方法,然后清除向量。
我唯一能想到的方法是添加一个getter(例如GetMean),但我不喜欢这个解决方案,也不认为它是最优雅的。
我该怎么办?
如果我要测试私有方法而不是私有变量,我该怎么做?

我一直想问类似的问题。但在我看来,单元测试和大型类并不搭配得很好。 - Konrad Rudolph
你难道看不到OtherClass中的效果吗? - R. Martinho Fernandes
你应该阅读《测试驱动开发的敌人之一:封装》(http://jasonmbaker.wordpress.com/2009/01/08/enemies-of-test-driven-development-part-i-encapsulation/)。 - André Caron
5
实际上,许多TDD的支持者认为封装已过时,不应再使用。这令人恼火。这篇文章探讨了这个方向,但却没有直接说出来。我在我的回答中给出了一个例子,其中使用一个应该被测试覆盖的私有方法是完全合理的。然后作者声称“可测试性是将某些东西公开的完全合理的原因”-不,不是这样。如果一个方法不应该被类的使用者使用(例如,因为它不能有实质性的使用),那么它就不应该是公共的。 - Konrad Rudolph
@Martinho:假设OtherClass调用另一个类中的方法,该方法又调用另一个类中的方法,以此类推。我认为依赖其他类的影响是有风险的(即:你不再测试单元,而是同时测试多个单元)。@André:感谢指引,但阅读后我和@Konrad持相同看法:我不喜欢将私有方法公开的想法。最多,可以使用条件编译(使用某些答案中提到的技巧)将它们公开。 - Jir
显示剩余4条评论
8个回答

68

单元测试应该测试单元,理想情况下每个类都是一个自包含的单元 - 这直接遵循单一职责原则。

因此,测试类的私有成员不应该是必要的 - 该类是一个黑盒子,可以按原样进行单元测试。

另一方面,并非总是如此,有时出于良好的原因(例如,类的多个方法可能依赖于应该进行测试的私有实用函数)。一个非常简单、非常差劲但最终成功的解决方案是将以下内容放入您的单元测试文件中,在包含定义类的头文件之前:

#define private public

当然,这破坏了封装性并且是 邪恶 的。但是对于测试来说,它能够达到目的。


2
看起来这个define是最不显眼的解决方案。我猜我面临的问题更多是因为我的类没有很好地遵循单一职责原则。关于我提出的玩具例子,你会建议我创建一个Mean类作为Variable的私有成员吗?这样Mean就可以毫无问题地进行测试了。然而,缺点是类的增加。 - Jir
6
尽管这个答案可适用于大多数编译器,但它不符合标准规范。你不能使用与关键字相同的 #define 语句。请参阅《C++98》第17.4.3.1.1节。 - Alexander Oh
1
难道友谊不比邪恶的定义更符合标准,而且以微不足道的封装成本实现测试是可能的吗? - paercebal
6
在任何公共/私有关键字之前的方法不会变为公共方法,顺便说一下。 - RiaD
@RiaD:遇到了这个问题。#define class struct 可以解决。 - Alexander Torstling
显示剩余14条评论

14

对于受保护的方法/变量,从该类派生一个Test类并进行测试。

对于私有方法/变量,引入友元类。这不是最好的解决方案,但可以为您完成工作。

或者使用此hack:

#define private public

6
在我看来,拥有一个“友元类”太过于侵入式了。 - Konrad Rudolph
@Konrad Rudolph - 是的,这就是为什么我说这不是最好的解决方案。而且我认为对于测试来说,这应该不是什么大问题。 - DumbCoder
2
定义关键字是未定义行为:https://dev59.com/zIbca4cB1Zd3GeqPX5vu - bolov
@KonradRudolph 我认为这并不会太过于侵入,因为只有你定义的友元类(实际上是你的测试)才能访问。如果你在谈论安全方面,那就完全是另一回事了:private/protected/public 并不能保护你的代码免受注入/反向工程等攻击,它只能保护你的代码免受误用(即不当编码)的影响,所以这也不是一个问题。 - avtomaton
@avtomaton 这不是关于安全性的问题。重点是已测试的类需要将测试类声明为“friend”,这意味着每当您添加新的单元测试类时,都需要添加非测试代码。 - Konrad Rudolph

9

总的来说,我同意其他人在这里所说的 - 只有公共接口应该进行单元测试。

然而,我最近遇到了一个情况,需要先调用一个受保护的方法,为特定的测试用例做准备。起初我尝试了上面提到的 #define protected public 方法; 这在 Linux/GCC 上可以工作,但在 Windows 和 Visual Studio 上失败了。

原因是将 protected 改为 public 也改变了符号名称并导致了链接器错误:库提供了一个 protected__declspec(dllexport) void Foo::bar() 方法,但是由于 #define 的存在,我的测试程序期望一个 public__declspec(dllimport) void Foo::bar() 方法,从而给我一个未解析的符号错误。

因此,基于 friend 的解决方案更适合我,我在类头文件中进行了以下操作:

// This goes in Foo.h
namespace unit_test {   // Name this anything you like
  struct FooTester; // Forward declaration for befriending
}

// Class to be tested
class Foo
{
  ...
private:
  bool somePrivateMethod(int bar);
  // Unit test access
  friend struct ::unit_test::FooTester;
};

在我的实际测试案例中,我做了这个:

#include <Foo.h>
#include <boost/test/unit_test.hpp>

namespace unit_test {
  // Static wrappers for private/protected methods
  struct FooTester
  {
    static bool somePrivateMethod(Foo& foo, int bar)
    {
      return foo.somePrivateMethod(bar);
    }
  };
}

BOOST_AUTO_TEST_SUITE(FooTest);
BOOST_AUTO_TEST_CASE(TestSomePrivateMethod)
{
  // Just a silly example
  Foo foo;
  BOOST_CHECK_EQUAL(unit_test::FooTester::somePrivateMethod(foo, 42), true);
}
BOOST_AUTO_TEST_SUITE_END();

这适用于Linux/GCC以及Windows和Visual Studio。

4

测试C++中受保护的数据的好方法是分配一个友元代理类:

#define FRIEND_TEST(test_case_name, test_name)\
friend class test_case_name##_##test_name##_Test

class MyClass
{
    private:
        int MyMethod();
        FRIEND_TEST(MyClassTest, MyMethod);
};

class MyClassTest : public testing::Test
{
    public:
      // ...
        void Test1()
        {
            MyClass obj1;
            ASSERT_TRUE(obj1.MyMethod() == 0);
        }

        void Test2()
        {
            ASSERT_TRUE(obj2.MyMethod() == 0);
        }

        MyClass obj2;
};

TEST_F(MyClassTest, PrivateTests)
{
    Test1();
    Test2();
}

查看更多Google测试(gtest)相关信息。


1

在我看来,测试一个类的私有成员/方法的需求是一种代码异味,但我认为在C++中这是可行的。

举个例子,假设你有一个Dog类,除了公共构造函数外,其余都是私有成员/方法:

#include <iostream>
#include <string>

using namespace std;

class Dog {
  public:
    Dog(string name) { this->name = name; };

  private:
    string name;
    string bark() { return name + ": Woof!"; };
    static string Species;
    static int Legs() { return 4; };
};

string Dog::Species = "Canis familiaris";

现在,由于某种原因,您想测试私有内容。您可以使用privablic来实现这一点。
同时,将所需的实现与privablic.h命名的头文件一起包含,如下所示:
#include "privablic.h"
#include "dog.hpp"

然后根据任何实例成员的类型映射一些存根。
struct Dog_name { typedef string (Dog::*type); };
template class private_member<Dog_name, &Dog::name>;

...和实例方法;

struct Dog_bark { typedef string (Dog::*type)(); };
template class private_method<Dog_bark, &Dog::bark>;

对所有静态实例成员执行相同的操作

struct Dog_Species { typedef string *type; };
template class private_member<Dog_Species, &Dog::Species>;

...和静态实例方法。

struct Dog_Legs { typedef int (*type)(); };
template class private_method<Dog_Legs, &Dog::Legs>;

现在你可以测试它们全部:
#include <assert.h>

int main()
{
    string name = "Fido";
    Dog fido = Dog(name);

    string fido_name = fido.*member<Dog_name>::value;
    assert (fido_name == name);

    string fido_bark = (&fido->*func<Dog_bark>::ptr)();
    string bark = "Fido: Woof!";
    assert( fido_bark == bark);

    string fido_species = *member<Dog_Species>::value;
    string species = "Canis familiaris";
    assert(fido_species == species);

    int fido_legs = (*func<Dog_Legs>::ptr)();
    int legs = 4;
    assert(fido_legs == legs);

    printf("all assertions passed\n");
};

输出:

$ ./main
all assertions passed

您可以查看test_dog.cppdog.hpp的源代码。

免责声明:感谢其他聪明人的见解,我已经组装了上述“库”,能够访问给定C++类的私有成员和方法,而不会改变其定义或行为。为使其正常工作,需要知道并包含类的实现(显然)。

注意:我根据评论员建议修改了此答案的内容。


2
请勿只简单地将某个工具或库作为答案发布。至少在答案中演示出 它如何解决问题 - Baum mit Augen

1

编写单元测试VariableImpl,以确保其行为正确,那么Variable也是正确的。

测试内部并不是世界上最糟糕的事情,但目标是只要接口合同得到保证,它们可以是任何东西。如果这意味着创建一堆奇怪的模拟实现来测试Variable,那就是合理的。

如果这似乎很多,考虑到实现继承并不能很好地分离关注点。如果难以进行单元测试,那对我来说就是一个非常明显的代码异味。


0

来自Google测试框架的示例:

// foo.h
#include "gtest/gtest_prod.h"
class Foo {
  ...
 private:
  FRIEND_TEST(FooTest, BarReturnsZeroOnNull);
  int Bar(void* x);
};

// foo_test.cc
...
TEST(FooTest, BarReturnsZeroOnNull) {
  Foo foo;
  EXPECT_EQ(0, foo.Bar(NULL));
  // Uses Foo's private member Bar().
}

主要思想是使用C++中的friend关键字。 您可以按以下方式扩展此示例:
// foo.h
#ifdef TEST_FOO
#include "gtest/gtest_prod.h"
#endif

class Foo {
  ...
 private:
  #ifdef TEST_FOO
  FRIEND_TEST(FooTest, BarReturnsZeroOnNull);
  #endif
  int Bar(void* x);
};

你可以用两种方式定义TEST_FOO预处理器符号:

  1. CMakeLists.txt文件中

     option(TEST "运行测试?" ON)
     if (TEST)
       add_definitions(-DTEST_FOO)
     endif()
    
  2. 作为编译器的参数

     g++ -D TEST $your_args
    

0

我通常建议测试类的公共接口,而不是私有/受保护的实现。在这种情况下,如果它不能通过公共方法从外部世界观察到,则单元测试可能不需要测试它。

如果功能需要子类,则可以对真实派生类进行单元测试,或创建自己的测试派生类,具有适当的实现。


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