C++ 在C+中测试私有类成员+;没有朋友

C++ 在C+中测试私有类成员+;没有朋友,c++,unit-testing,private,protected,members,C++,Unit Testing,Private,Protected,Members,今天我和一位同事讨论了是否测试班级中的私人成员或私人状态。他几乎让我相信了这是有道理的。此问题的目的不是重复关于测试私人成员的性质和原因的现有问题,例如: 在我看来,同事们的建议将友元声明引入单元测试实现类有点脆弱。在我看来,这是不可能的,因为我们在测试代码中引入了一些测试代码的依赖性,而测试代码已经依赖于测试代码=>循环依赖性。即使是像重命名测试类这样的无辜行为也会破坏单元测试,并在测试代码中强制执行代码更改 我想请C++大师对另一个建议进行判断,这取决于我们可以专门化一个模板函数。想象一下这

今天我和一位同事讨论了是否测试班级中的私人成员或私人状态。他几乎让我相信了这是有道理的。此问题的目的不是重复关于测试私人成员的性质和原因的现有问题,例如:

在我看来,同事们的建议将友元声明引入单元测试实现类有点脆弱。在我看来,这是不可能的,因为我们在测试代码中引入了一些测试代码的依赖性,而测试代码已经依赖于测试代码=>循环依赖性。即使是像重命名测试类这样的无辜行为也会破坏单元测试,并在测试代码中强制执行代码更改

我想请C++大师对另一个建议进行判断,这取决于我们可以专门化一个模板函数。想象一下这个班级:

// tested_class.h

struct tested_class 
{
  tested_class(int i) : i_(i) {}

  //some function which do complex things with i
  // and sometimes return a result

private:
  int i_;
};
// tested_class.h

struct tested_class 
{
  explicit
  tested_class(int i=0) : i_(i) {}

  template<class Ctx>
  static void test_backdoor(Ctx& ctx);

  //some function which do complex things with i
  // and sometimes return a result

private:
  int i_;
};
我不喜欢仅仅为了让它可测试而给我一个getter的想法。所以我的建议是在类中声明“test_backdoor”函数模板:

// tested_class.h

struct tested_class 
{
  tested_class(int i) : i_(i) {}

  //some function which do complex things with i
  // and sometimes return a result

private:
  int i_;
};
// tested_class.h

struct tested_class 
{
  explicit
  tested_class(int i=0) : i_(i) {}

  template<class Ctx>
  static void test_backdoor(Ctx& ctx);

  //some function which do complex things with i
  // and sometimes return a result

private:
  int i_;
};
如果使用朋友,我必须将每个测试用例声明为朋友,然后。。。或者最终在一些常见类型(如fixture)中引入一些测试功能,将其声明为朋友,并将所有测试调用转发给该类型。。。这不是很奇怪吗


我希望看到你的支持者和反对者实践这种方法。

我通常不觉得有必要对私有成员和函数进行单元测试。我可能更愿意引入一个公共函数来验证正确的内部状态

但是,如果我真的决定在细节中四处窥探,我会在单元测试程序中使用一个讨厌的快速攻击

#include <system-header>
#include <system-header>
// Include ALL system headers that test-class-header might include.
// Since this is an invasive unit test that is fiddling with internal detail
// that it probably should not, this is not a hardship.

#define private public
#include "test-class-header.hpp"
...
#包括
#包括
//包括测试类标头可能包含的所有系统标头。
//因为这是一个侵入性的单元测试,需要处理内部细节
//也许不应该,这不是一个困难。
#定义私人和公共
#包括“测试类标题.hpp”
...

Linux上的P>至少可以这样做,因为C++名称的不包含私有/公共状态。有人告诉我,在其他系统上,这可能不是真的,它不会链接。

测试私有成员并不总是通过检查状态是否等于某些预期值来验证状态。为了适应其他更复杂的测试场景,我有时会使用以下方法(此处简化以传达主要思想):

//公共头
结构IFoo
{
公众:
虚拟~IFoo(){}
虚空DoSomething()=0;
};
std::shared_ptr CreateFoo();
//专用测试头
结构IFooInternal:公共IFoo
{
公众:
虚拟~IFooInternal(){}
虚拟void DoSomethingPrivate()=0;
};
//实现头
Foo类:公共IFOO内部
{
公众:
虚拟剂量仪();
虚拟空隙DoSomethingPrivate();
};
//测试代码
std::共享\u ptr p=
std::dynamic_pointer_cast(CreateFoo());
p->DoSomethingPrivate();

这种方法具有明显的优点,可以促进良好的设计,并且不会弄乱好友声明。当然,大多数时候你不必经历这些麻烦,因为能够测试私有成员是一个非常不标准的要求。

我认为单元测试是测试被测试类的可观察行为。因此,没有必要测试私有部件,因为它们本身是不可见的。测试它的方法是测试对象的行为是否符合预期(这暗示所有私有内部状态都是有序的)

不必担心私有部分的原因是,通过这种方式,您可以更改实现(例如重构),而无需重写测试

因此,我的答案是不要这样做(即使技术上可能),因为这违背了单元测试的理念。

Pros

  • 您可以访问私有成员来测试它们
  • 这是相当少的
    hack
缺点

  • 包装破损
  • 破损的封装更加复杂,就像
    friend
  • test\u backdoor
    放在生产侧,将测试与生产代码混合
  • 维护问题(就像将测试代码友好化一样,您已经与测试代码建立了非常紧密的耦合)

撇开所有的优点/缺点不谈,我认为您最好进行一些架构更改,以便更好地测试正在发生的任何复杂事情

可能的解决方案

  • 使用Pimpl习惯用法,将
    复杂的
    代码与私有成员一起放入Pimpl中,并为Pimpl编写一个测试。Pimpl可以被前向声明为公共成员,从而允许在单元测试中进行外部实例化。Pimpl只能由公共成员组成,因此更易于测试
    • 缺点:代码太多
    • 缺点:不透明类型,调试时更难看到内部
  • 只需测试类的公共/受保护接口。测试您的接口所列出的契约。
    • 缺点:单元测试很难/不可能以孤立的方式编写
  • 与Pimpl解决方案类似,但创建一个包含
    复杂
    代码的自由函数。将声明放在私有头中(不是库公共接口的一部分),并测试它
  • 通过试验方法/夹具破坏封装
    • 可能的变化:声明
      friend struct test\u上下文,将测试代码放入
      struct test\u context
      实现中的方法中。这样,您就不必为每个测试用例、方法或夹具交朋友。这会降低有人破坏友谊的可能性
  • 破镜重圆
    // Public header
    struct IFoo
    {
    public:
        virtual ~IFoo() { }
        virtual void DoSomething() = 0;
    };
    std::shared_ptr<IFoo> CreateFoo();
    
    // Private test header
    struct IFooInternal : public IFoo
    {
    public:
        virtual ~IFooInternal() { }
        virtual void DoSomethingPrivate() = 0;
    };
    
    // Implementation header
    class Foo : public IFooInternal
    {
    public:
        virtual DoSomething();
        virtual void DoSomethingPrivate();
    };
    
    // Test code
    std::shared_ptr<IFooInternal> p =
        std::dynamic_pointer_cast<IFooInternal>(CreateFoo());
    p->DoSomethingPrivate();
    
    class Something{
       PRIVATE:
           int m_attr;
    };
    
    /* test_tag.h */
    #ifndef TEST_TAG_H_INCLUDED_
    #define TEST_TAG_H_INCLUDED_
    
    template <typename Tag, typename Tag::type M>
    struct Rob
    {
        friend typename Tag::type get(Tag)
        {
            return M;
        }
    };
    
    template <typename Tag, typename Member> 
    struct TagBase
    {
        typedef Member type;
        friend type get(Tag);
    };
    
    
    #endif /* TEST_TAG_H_INCLUDED_ */
    
    /* tested_class.h */
    #ifndef TESTED_CLASS_H_INCLUDED_
    #define TESTED_CLASS_H_INCLUDED_
    
    #include <string>
    
    struct tested_class
    {
        tested_class(int i, const char* descr) : i_(i), descr_(descr) { }
    
    private:
        int i_;
        std::string descr_;
    };
    
    /* with or without the macros or even in a different file */
    #   ifdef TESTING_ENABLED
    #   include "test_tag.h"
    
        struct tested_class_i : TagBase<tested_class_i, int tested_class::*> { };
        struct tested_class_descr : TagBase<tested_class_descr, const std::string tested_class::*> { };
    
        template struct Rob<tested_class_i, &tested_class::i_>;
        template struct Rob<tested_class_descr, &tested_class::descr_>;
    
    #   endif
    
    #endif /* TESTED_CLASS_H_INCLUDED_ */
    
    /* test_access.cpp */
    #include "tested_class.h"
    
    #include <cstdlib>
    #include <iostream>
    #include <sstream>
    
    #define STRINGIZE0(text) #text
    #define STRINGIZE(text) STRINGIZE0(text)
    
    int assert_handler(const char* expr, const char* theFile, int theLine)
    {
        std::stringstream message;
        message << "Assertion " << expr << " failed in " << theFile << " at line " << theLine;
        message << "." << std::endl;
        std::cerr << message.str();
    
        return 1;
    }
    
    #define ASSERT_HALT() exit(__LINE__)
    
    #define ASSERT_EQUALS(lhs, rhs) ((void)(!((lhs) == (rhs)) && assert_handler(STRINGIZE((lhs == rhs)), __FILE__, __LINE__) && (ASSERT_HALT(), 1)))
    
    int main()
    {
        tested_class foo(35, "Some foo!");
    
        // the bind pointer to member by object reference could
        // be further wrapped in some "nice" macros
        std::cout << " Class guts: " << foo.*get(tested_class_i()) << " - " << foo.*get(tested_class_descr()) << std::endl;
        ASSERT_EQUALS(35, foo.*get(tested_class_i()));
        ASSERT_EQUALS("Some foo!", foo.*get(tested_class_descr()));
    
        ASSERT_EQUALS(80, foo.*get(tested_class_i()));
    
        return 0; 
    }
    
    #define private public