【问题标题】:Unit test accessing private variables单元测试访问私有变量
【发布时间】:2011-12-23 17:03:35
【问题描述】:

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

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

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

我有以下选择:

  • 使 m_variable public - 丑陋
  • 制作方法test_getVariable() - 过于复杂
  • friend class Tester 添加到 Working - 然后 Working 明确地“了解”测试人员,这不好

我的理想是

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 但不是每个测试...但它不起作用。显然,友谊不适用于继承。

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

【问题讨论】:

标签: c++ unit-testing


【解决方案1】:

-fno-access-control

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

【讨论】:

    【解决方案2】:

    我同意Trott's answer,但有时您会将单元测试添加到不是为它设计的遗留代码中。在这些情况下,我会联系#define private public。它只是在单元测试中,它只是用于重构太昂贵而无法打扰的时候。这很丑陋,在技术上是非法的,而且非常有效。

    【讨论】:

    • 即便如此,我也会使用-D private=public 或类似的编译器语句。
    • 真的吗?这将允许测试从需要它的代码中编译出来的魔力移开。我不想把这种东西埋在构建系统的某个地方。我喜欢我的丑陋黑客正面和中心,每个人都可以看到它们。
    • 而且,如果你在需要它的`include 之后#undef private,你可以将损坏限制在一个地方。
    • @Kristo 说得对。 遗留代码的初始测试不是真正的单元测试。这是一个测试,因此当您更改依赖项等时,您可以确信自己没有破坏代码。
    【解决方案3】:

    通常,您的单元测试不应评估私有变量。将测试写入接口,而不是实现。

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

    https://stackoverflow.com/a/1093481/436641 有一个更长的答案(为 C# 而不是 C++ 编写,但适用相同的原则)。

    【讨论】:

    • 这样的声明(永远不应该测试私有接口)让我感到困惑。让我稍微探讨一下:例如,一个工厂应该在所有客户端释放资源后将借出的资源归还给空闲池。由于不需要向客户端公开池状态,因此它是私有的。在这种情况下应该如何验证正确的行为? (根据行为发生变化的任何情况,如果后来有人出现并实施了不同的方案,我希望这些测试需要重构。事实上,他们会亮起红色并告诉实施者,这是设计使然。)
    • FWIW,答案现在说“一般”以明确表示存在例外。
    【解决方案4】:

    努力使用您的公共接口测试您的所有私有代码。不仅最初的工作量较少,而且您更改实现时,单元测试仍然有效的可能性要高得多。

    也就是说,有时您只需要戳内部结构即可获得良好的测试覆盖率。在这种情况下,我使用了一个称为expose 的成语。仔细想想,里面有一个笑话。

    需要测试的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),所以我认为最大化单元测试工作机会不一定是好的。
    • 假设您编写了一个使用列表实现的调度程序。稍后您会发现性能问题并切换到阵列。所有相同的功能都需要进行测试。但是,如果原始测试依赖于实现细节中的列表,它们将不再编译。您将不得不重写它们或完全废弃它们。但是,如果您编写的单元测试只涉及公共接口,那么这些相同的单元测试应该可以正常工作,如果幸运的话,它们仍然可以提供足够的覆盖率。
    • 嗯。由于从列表更改为数组而导致私有函数测试失败的任何测试仍然会导致公共函数测试失败,不是吗?我看到的唯一优势是你可以做更少的测试,这可能意味着你不够彻底。此外,IMO 看到一堆失败的测试是一件好事,因为您现在可以更详细地了解您可能导致的问题。
    【解决方案5】:

    如果您绝对必须这样做,您可以有条件地编译您的代码,以便 TestBase 仅在单元测试时成为朋友:

    class Working {
        // ...
        private:
        int m_variable;
    
    #ifdef UNIT_TESTING
        friend class TestBase;
    #endif
    };
    

    【讨论】:

      【解决方案6】:

      我通过在测试中使用缺少“私有”访问说明符的类头文件的副本来做到这一点。该副本由 test 目录中的 makefile 生成,以便在原始更改时重新生成副本:

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

      而“测试”的 make 目标取决于 mock_class_header.h。

      这将授予对测试中所有私有成员变量的访问权限,即使在编译真实库时这些成员变量是私有的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-11-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-19
        • 2019-09-27
        相关资源
        最近更新 更多