测试私有类成员在C ++中没有朋友的[复制](Testing private class membe

2019-07-30 09:46发布

这个问题已经在这里有一个答案:

  • 如何测试一个私有函数或具有私有方法,字段或内部类的类? 48个回答

今天,我不得不对是否测试或不测试私有成员或私有状态班上一位同事的讨论。 他几乎说服了我,为什么它是有道理的。 这个问题的目的并不是要复制的性质和测试的私有成员一样的原因,已经存在的StackOverflow的问题: 什么是错做单元测试的类,它正在测试的朋友吗?

同事建议是在我看来有点脆弱,介绍朋友声明单元测试实现类。 在我看来,这是一个没有去,因为我们引进的测试代码,测试代码一些依赖,而测试代码已经取决于测试的代码=>循环依赖。 即使这样的无辜之类的东西在断开单元测试和执行在测试的代码的代码更改重命名测试类的结果。

我想问问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_;
};

我不喜欢这个主意有一个getter方法I - 这只是为了测试。 所以我的建议是“test_backdoor”在类的函数模板声明:

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

只需添加这个功能,我们可以使类的私有成员测试。 请注意,没有依赖于单元测试类,也不是模板函数的实现。 在这个例子中,单元测试实现使用升压测试框架。

// tested_class_test.cpp

namespace
{
  struct ctor_test_context
  {
    tested_class& tc_;
    int expected_i;
  };
}

// specialize the template member to do the rest of the test
template<>
void tested_class::test_backdoor<ctor_test_context>(ctor_test_context& ctx)
{
  BOOST_REQUIRE_EQUAL(ctx.expected_i, tc_.i_);
}

BOOST_AUTO_TEST_CASE(tested_class_default_ctor)
{
  tested_class tc;
  ctor_test_context ctx = { tc, 0 };
  tested_class::test_backdoor(ctx);
}

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  tested_class tc(-5);
  ctor_test_context ctx = { tc, -5 };
  tested_class::test_backdoor(ctx);
}

通过引入只是一个单一的模板声明,这是不可调用的所有,我们给测试实施者的可能性转发测试逻辑成一个函数。 功能,作用于类型安全的环境,是从特定的测试编译单元内唯一可见的,由于测试方面的匿名类型性质。 最重要的事是,我们可以定义为许多匿名的测试环境,因为我们喜欢并且擅长在他们的测试中,无需接触测试类。

当然,用户必须知道什么模板专业化是,但是这个代码实在不好或怪异或无法读取? 或者,我可以从C ++开发人员期望有知识模板专业化是什么C ++以及它是如何工作的?

在阐述用朋友申报的单元测试类,我不认为这是稳健的。 试想一下,升压架构(或者可能是其他测试框架)。 它为每个测试情况下,单独的类型。 但是,为什么要我,只要我能写不在意:

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  ...
}

如果使用的朋友,我不得不宣布每个测试用例作为一个朋友,然后......或者结束在一些常见的类型(如夹具)引入一些测试功能,声明它作为一个朋友,并转发所有的测试呼叫该类型.. 。是不是很奇怪吗?

我想看到你的利弊练习这种方法。

Answer 1:

接下来的就是不从技术上来讲直接回答你的问题,因为它仍然使用了“朋友”的功能,但它并不需要被测试的实体本身的修改,我认为这addesses打破提到的一些封装的关注的其他答案; 它虽然需要写一些样板代码。

其背后的想法是不是我的,实现完全基于提出并通过他的博客litb解释一招 (加上本萨特gotw只是多一点点的背景下,至少对我来说) -总之CRTP,朋友, ADL和指针成员(我必须承认,我的不舍ADL部分我还是不明白这一点完全是,但我relentesly中想出来的100%的工作)。

我用gcc 4.6进行了测试,铛3.1和VS2010的编译器和它完美的作品。

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


Answer 2:

我觉得单元测试是关于测试被测类的观察到的行为。 因此,没有必要测试私处,因为他们自己都无法观测。 你测试的方法是通过测试对象的行为是否您希望它(这隐含意味着所有的私有内部状态是按顺序)的方式。

之所以不被关注的私处是,这种方式可以改变实现(如重构),而不必重写你的测试。

所以,我的回答是,因为它违背了单元测试的理念不这样做(即使技术上是可行的)。



Answer 3:

优点

  • 您可以访问私有成员对它们进行测试
  • 它的一个相当少量的hack

缺点

  • 残破的封装
  • 残破的封装是比较复杂,就像易碎的friend
  • 通过将混合生产代码的测试test_backdoor在生产方面
  • 维修保养问题(就像邀请好友的测试代码,您已经创建了一个非常紧密的耦合与你的测试代码)

所有优点/缺点一边的,我觉得你是最好的关闭使得一些建筑的变化,让一切复杂的东西正在发生的更好的测试。

可能的解决方案

  • 使用PIMPL方法,把complex代码在平普尔与私有成员一起,谱写了平普尔测试。 的平普尔可以向前声明为公用部件,从而允许在单元测试外部实例化。 平普尔只能包含公共成员,使其更容易测试
    • 缺点:大量的代码
    • 缺点:不透明的类型,可以是比较难见到的内部调试的时候
  • 只是测试类的公共/保护的接口。 测试你的接口规定了合同。
    • 缺点:单元测试是困难的/不可能的隔离方式来写。
  • 类似于平普尔的解决方案,而是创建一个免费的功能, complex在它的代码。 把申报专用标题(库的一部分不是公共接口),并对其进行测试。
  • 经由朋友的测试方法/夹具破坏了封装
    • 在这个可能的变化:申报friend struct test_context; ,把你的测试代码的方法内部在执行struct test_context 。 这样,您就不必朋友每个测试用例,方法,或夹具。 这应该降低其他人打破了邀请好友的情形产生。
  • 通过模板特打破封装


Answer 4:

我很抱歉地通知这一点,但它帮助我当这些答案大多数方法也不是没有强大的重构实现:添加头之前用其私有成员,你要访问的类文件,

#define private public

它是邪恶的,但

  • 不与生产代码干扰

  • 作为朋友/更改访问级别确实不破坏封装

  • 避免了与PIMPL方法重重构

所以你可能去了...



Answer 5:

测试私有成员并不一定需要通过检查,如果它等于一些期望值验证的状态。 为了适应其它更复杂的测试方案,我有时使用以下的方法(这里简化传达的主要思想):

// 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();

这种方法具有促进良好的设计和不凌乱与朋友申报的独特优势。 当然,你不必经过麻烦的大部分时间,因为能够测试私有成员是开始与一个漂亮的非标准的要求。



Answer 6:

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

但是,如果我决定去在细节上闲逛,我使用的单元测试程序讨厌快速破解

#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"
...

在Linux上至少这个工作,因为C ++的名字改编不包括私有/公共状态。 据我所知,在其他系统上,这可能不是真实的,它不会链接。



Answer 7:

我用一个函数来测试私有类成员这只是所谓的TestInvariant()。

它是类的私有成员,并在调试模式下,被称为在每个函数(除了构造函数和dctor结束的开始)的开头和结尾。

这是虚拟和任何基类调用之前它自己的父版本。

这让我验证类的内部状态,所有的时间没有阶级的intenals暴露给任何人。 我有很简单的测试,但没有任何理由,你为什么不能有复杂的问题,甚至可以将其打开或关闭用标志等。

您也可以可以通过打电话给你TestInvariant()函数,其他类称为公共测试功能。 因此,当您需要更改内部类的运作不需要更改任何用户代码。

这会帮助吗?



Answer 8:

我觉得要问的第一件事就是:为什么朋友认为是东西,必须谨慎使用?

因为它打破封装。 它提供了另一个类或函数访问你的对象的内部,从而扩大你的私有成员的可见范围。 如果你有很多的朋友,这是更难推理的对象的状态。

在我看来,模板解决方案比朋友在这方面更是雪上加霜。 模板的你的主要规定的好处是,你不再需要明确地朋友从类测试。 我认为,恰恰相反,这是不利的。 有两个原因。

  1. 测试连接到您的类的内部。 任何人都改变类应该知道,通过改变,他们可能会打破试验对象的士兵。 朋友告诉他们什么对象可以被连接到你的类的内部状态,但模板解决方案不。

  2. 朋友限制阴部的范围扩大。 如果您的朋友一类,你知道只有类可以访问你的内部。 因此,如果你的朋友的测试,你知道,只有测试可以读取或写入到私有成员变量。 您的模板后门,但是,可以在任何地方使用。

该模板解决方案是无效的,因为它隐藏的问题,而不是修复它。 与循环依赖的根本问题依然存在:有人改变类必须了解每一个使用后门,有人改变测试必须知道的类。 基本上,只是通过在一种迂回的方式全部私人数据到公共数据删除从类测试的参考。

如果你必须从你的测试,只是朋友测试夹具访问私有成员,用它做。 这是简单易懂。



Answer 9:

有一种理论认为,如果是私人它不应该被单独测试,如果需要这样的话,应该重新设计。

对我来说,这是什叶派。

在某些项目的人创建的私有方法中宏,就像:

class Something{
   PRIVATE:
       int m_attr;
};

当编译测试PRIVATE被定义为公共的,否则它被定义为私有。 就这么简单。



文章来源: Testing private class member in C++ without friend [duplicate]