在没有朋友的情况下用C ++测试私有类成员

时间:2012-07-12 18:26:36

标签: c++ unit-testing private protected members

今天我与一位同事讨论了是否要测试是否在课堂上测试私人会员或私人州。他几乎让我相信为什么它有意义。此问题并非旨在复制已存在的有关测试私有成员的性质和原因的StackOverflow问题,例如:What is wrong with making a unit test a friend of the class it is testing?

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

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

我不喜欢让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_;
};

通过添加此函数,我们可以使类的私有成员可测试。注意,不依赖于单元测试类,也不依赖于模板函数实现。在此示例中,单元测试实现使用Boost测试框架。

// 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 ++模板专业化是什么以及它是如何工作的?

详细说明使用friend来声明单元测试类我不认为这是强大的。想象一下boost框架(或者可能是其他测试框架)。它为每个测试用例生成一个单独的类型。但是,为什么我应该关心,只要我能写:

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  ...
}

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

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

9 个答案:

答案 0 :(得分:21)

我认为单元测试是关于测试被测试类的可观察行为。因此,不需要测试私有部件,因为它们本身是不可观察的。测试它的方法是测试对象是否按照您期望的方式运行(这隐含意味着所有私有内部状态都按顺序排列)。

不关心私有部分的原因是这样你可以改变实现(例如重构),而不必重写你的测试。

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

答案 1 :(得分:6)

<强>赞成

  • 您可以访问私人会员进行测试
  • 其数量相当少hack

<强>缺点

  • 破坏封装
  • 破碎的封装比friend
  • 更复杂,更脆弱
  • test_backdoor放在生产方
  • ,将测试与生产代码混合
  • 维护问题(就像对测试代码进行交流一样,您已经创建了
  • 紧密耦合的测试代码)

除了所有优点/缺点之外,我认为你最好做一些架构改变,以便更好地测试正在发生的任何复杂事情。

可能的解决方案

  • 使用Pimpl习语,将complex代码与私有成员一起放入pimpl中,并为Pimpl编写测试。 Pimpl可以被声明为公共成员,允许在单元测试中进行外部实例化。 Pimpl只能由公共成员组成,因此更容易测试
    • 缺点:很多代码
    • 缺点:在调试时可能更难以看到的opaque类型
  • 只需测试该类的公共/受保护接口。测试您的界面所列出的合同。
    • 缺点:单元测试很难/不可能以孤立的方式编写。
  • 与Pimpl解决方案类似,但创建了一个带有complex代码的免费功能。将声明放在私有标头(不是库公共接口的一部分)中,并测试它。
  • 通过朋友测试方法/夹具打破封装
    • 可能的变化:声明friend struct test_context;,将测试代码放在struct test_context实现中的方法中。这样您就不必为每个测试用例,方法或夹具做好事。这应该可以减少某人破坏朋友的可能性。
  • 通过模板专业化打破封装

答案 2 :(得分:2)

从技术上讲,接下来的内容不是对你的直接回答 问题,因为它仍然会使用“朋友”功能 但它不需要修改被测实体本身 我认为它破坏了打破封装的担忧 在其他一些答案中提到;它确实需要 写一些样板代码。

背后的想法不是我的,实施是 entirely based on a trick presented and explained by litb on his blog(加上此Sutter's gotw只是一点点 更多上下文,至少对我来说) - 简而言之,CRTP,朋友,ADL和指向成员的指针 (我必须承认,令我沮丧的是ADL部分,我仍然没有 完全得到它,但我正在努力将其弄清楚100%)。

我用gcc 4.6,clang 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; 
}

答案 3 :(得分:2)

我很遗憾地建议这一点,但是如果没有强大的重构,这些答案中的大多数方法都无法实现,这对我有所帮助:在文件的标题前面加上你想要访问的私有成员的类,

#define private public

这是邪恶的,但

  • 不会干扰生产代码

  • 不会破坏封装,因为朋友/更改访问级别

  • 避免使用PIMPL习语进行大量重构

所以你可以去...

答案 4 :(得分:1)

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

但是,如果我决定在详细信息中进行讨论,我会在单元测试程序中使用讨厌的快速破解

#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 ++名称修改不包括私有/公共状态。我被告知,在其他系统上,这可能不是真的,也不会链接。

答案 5 :(得分:1)

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

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

这种方法具有促进良好设计的独特优势,并且不会因朋友声明而变得混乱。当然,大多数时候你不必经历麻烦,因为能够测试私人成员是一个非常非标准的要求。

答案 6 :(得分:1)

我使用了一个函数来测试私有类成员,它只是名为TestInvariant()。

它是该类的私有成员,并且在调试模式下,在每个函数的开头和结尾调用(除了ctor的开头和dctor的结尾)。

它是虚拟的,任何基类都称为父版本。

这使我能够一直验证班级的内部状态,而不会将班级的意图暴露给任何人。我进行了非常简单的测试,但是你没有理由不能使用复杂的测试,甚至没有使用旗帜等设置它的开关。

此外,您还可以使用公共测试函数,这些函数可以由调用TestInvariant()函数的其他类调用。因此,当您需要更改内部类工作时,您不需要更改任何用户代码。

这会有帮助吗?

答案 7 :(得分:1)

我认为首先要问的是:为什么朋友被认为是必须谨慎使用的东西?

因为它破坏了封装。它提供了另一个类或函数,可以访问对象的内部,从而扩展私有成员的可见范围。如果你有很多朋友,那就很难推断出你的对象的状态。

在我看来,模板解决方案在这方面甚至比朋友差。您对模板的主要好处是您不再需要明确地与该类测试相关。我认为,相反,这是一种损害。这有两个原因。

  1. 测试与您班级的内部相关联。任何改变类的人都应该知道通过改变对象的私有性,他们可能会破坏测试。朋友告诉他们确切的对象可能与你班级的内部状态有关,但模板解决方案没有。

  2. 朋友限制了您的私有范围扩展。如果你是一个班级的朋友,你知道只有那个班级可以访问你的内部。因此,如果您对测试进行了测试,则您知道只有测试可以读取或写入私有成员变量。但是,您的模板后门可以在任何地方使用。

  3. 模板解决方案无效,因为它隐藏了问题而不是修复问题。循环依赖的根本问题仍然存在:更改类的人必须知道后门的每次使用,并且更改测试的人必须知道该类。基本上,只有通过以迂回的方式将所有私人数据转换为公共数据,才能删除对该类测试的引用。

    如果您必须从测试中访问私人成员,请与测试夹具联系并完成测试。这很简单易懂。

答案 8 :(得分:0)

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

对我而言,这是什叶派。

在某些项目中,人们为私有方法创建一个宏,就像:

class Something{
   PRIVATE:
       int m_attr;
};

编译测试时,PRIVATE被定义为public,否则它被定义为private。就这么简单。