单元测试访问私有变量

8

我有一个单元测试类Tester;我希望它能够访问Working类的私有字段。

class Working {
    // ...
    private:
    int m_variable;
};

class Tester {
    void testVariable() {
        Working w;
        test( w.m_variable );
    }
}

我有以下几个选项:
  • 将m_variable设为public - 这样做不好看
  • 编写test_getVariable()方法 - 这样会过于复杂
  • 在Working类中加入friend class Tester - 这样Working就会显式地知道Tester,这并不是一个好的选择

我理想的方案是

class Working {
    // ...
    private:
    int m_variable;

    friend class TestBase;
};

class TestBase {};

class Tester : public TestBase {
    void testVariable() {
        Working w;
        test( w.m_variable );
    }
}

Working知道TestBase,但不知道每个测试用例...但这种方法行不通。显然,友谊不能和继承一起使用。

在这里最优雅的解决方案是什么?


6个回答

15

我同意Trott的回答,但有时候你需要为没有编写单元测试的遗留代码添加单元测试。在这种情况下,我会使用#define private public。这仅适用于单元测试,并且仅在重构成本太高而难以处理时才使用。虽然这样做很丑陋,而且从技术上来说是违法的,但非常有效。


2
即便如此,我仍会选择“-D private=public”或类似的编译器声明。 - Etienne de Martel
4
真的吗?这将使测试编译所需的魔法移出需要它的代码。我不想把这种东西埋在构建系统中。我喜欢我的丑陋的黑科技放在众人眼前,让所有人都能看到。 - Michael Kristofik
3
如果您在需要它的包含文件后立即使用 #undef private,则可以将损坏限制在一个地方。 - Michael Kristofik
2
哈哈,如果John Carmack能做到,你也能! :D - Kos
2
@Kristo 是正确的。对于遗留代码(Legacy Code)的初始测试并不是真正的单元测试。这是一种测试方式,当您更改依赖关系等内容时,可以确信您没有破坏代码。 - Gutzofter

14

通常情况下,你的单元测试不应该评估私有变量。编写测试时应该关注接口而非实现。

如果你确实需要检查某个私有变量是否具有特定特征,请考虑使用 assert() 而不是尝试为其编写单元测试。

更详细的答案(针对 C# 而非 C++,但同样适用)请参见 https://dev59.com/hnNA5IYBdhLWcg3wGJwV#1093481


4
像“私有接口不应该被测试”的说法让我感到困惑。让我深入探讨一下:举个例子,假设一个工厂应该在所有客户端都释放资源时将其归还到空闲池中。由于客户端无需知道池的状态,因此它是私有的。在这种情况下,如何验证正确的行为?(就像任何行为发生改变的情况一样,如果有人后来实现了不同的方案,我期望这些测试需要进行重构。事实上,它们会变成红色并告诉实现者,这也是设计上的考虑。) - U007D
顺便说一句,现在的答案已经加上了“通常”这个词以明确存在例外情况。 - Trott

5

-fno-access-control

如果你只使用GCC编译器,你可以在编译单元测试时使用编译器选项-fno-access-control。这将导致GCC跳过所有访问检查,但仍保持类的布局不变。我不知道其他编译器是否有类似的选项,所以这不是一个通用的解决方案。


3

尽力使用公共接口测试所有私有代码。这不仅在最初减少了工作量,而且当你改变实现时,单元测试仍能高概率工作。

话虽如此,有时候您需要挖掘内部以达到良好的测试覆盖率。在这种情况下,我使用一种我称之为“暴露”的惯用语法。如果您考虑一下,其中包含一个笑话。

需要测试的Foo类

class Foo
{
public:
   // everyone is on their honor to only use Test for unit testing.
   // Technically someone could use this for other purposes, but if you have
   // coders purposely doing bad thing you have bigger problems.
   class Test;

   void baz( void );

private:
   int m_int;
   void bar( void );
};

foo_exposed.h仅供单元测试代码使用。

class Foo::Test : public Foo
{
public:
   // NOTE baz isn't listed

   // also note that I don't need to duplicate the
   // types / signatures of the private data.  I just
   // need to use the name which is fairly minimal.

   // When i do this I don't list every private variable here.
   // I only add them as I need them in an actual unit test, YAGNI.

   using Foo::m_int;
   using Foo::bar;
};


// yes I'm purposely const smashing here.
// The truth is sometimes you need to get around const
// just like you need to get around private

inline Foo::Test& expose( const Foo& foo )
{
   return * reinterpret_cast<Foo::Test*>(
         &const_cast<Foo::Test&>( foo )
      );
}

如何在单元测试代码中使用它。
#include "foo_exposed.hpp"

void test_case()
{
   const Foo foo;

   // dangerous as hell, but this is a unit test, we do crazy things
   expose(foo).m_int = 20;
   expose(foo).baz();
}

当您更改实现时,单元测试仍能正常工作的可能性要高得多。 - 你能解释一下为什么你想要这个吗?因为我认为如果你进行了一个会破坏一些东西的更改,你希望单元测试告诉你这是如何发生的。此外,让所有单元测试都能正常工作的最高可能性就是编写一个测试,例如assert.istrue(true),因此我认为最大化单元测试工作机会并不一定是好事。 - puser
假设你编写了一个使用列表实现的调度程序。后来你发现性能有问题,于是改用数组。所有相同的功能都需要进行测试。但是如果原始测试依赖于实现细节中的列表,它们将不再编译。你必须重新编写它们或完全放弃它们。然而,如果你编写的单元测试只涉及公共接口,那么这些单元测试应该可以正常工作,如果你很幸运,它们仍然提供足够的覆盖率。 - deft_code
嗯,任何因从列表更改为数组而失败的私有函数测试仍应导致公共函数测试失败,对吧?我唯一看到的优点是你可以做更少的测试,这可能意味着你不够彻底。此外,在我看来,看到一堆失败的测试是件好事,因为现在你知道了更多关于你可能引起的问题的细节。 - puser

2
如果你非常需要这样做,你可以有条件地编译你的代码,这样当进行单元测试时,TestBase只是一个友元类:
class Working {
    // ...
    private:
    int m_variable;

#ifdef UNIT_TESTING
    friend class TestBase;
#endif
};

0

我通过在我的测试中使用一个缺少“private”访问说明符的类头文件的副本来实现这一点。该副本是由测试目录中的makefile生成的,因此如果原始文件发生更改,则会重新生成该副本:

 perl -ne 'print unless m/private:/;' < ../include/class_header.h > mock_class_header.h

'test'的make目标依赖于mock_class_header.h。

这允许访问测试中的所有私有成员变量,即使真正的库是使用这些成员变量作为私有编译的。


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