17

今天我和一位同事讨论了在课堂上是否测试私有成员或私有状态。他几乎说服了我为什么这样做是有道理的。这个问题的目的不是重复已经存在的关于测试私有成员的性质和原因的 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_;
};

我不喜欢为 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 Test 框架。

// 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 框架(或者可能是其他测试框架)。它为每个测试用例生成一个单独的类型。但我为什么要关心,只要我能写:

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  ...
}

如果使用朋友,我必须将每个测试用例声明为朋友......或者最终以某种常见类型(如夹具)引入一些测试功能,将其声明为朋友,并将所有测试调用转发到该类型...... . 这不是很奇怪吗?

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

4

9 回答 9

21

我认为单元测试是关于测试被测类的可观察行为。因此没有必要测试私处,因为它们本身是不可观察的。您测试它的方式是测试对象是否按照您期望的方式运行(这隐含地暗示所有私有内部状态都是有序的)。

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

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

于 2012-07-17T01:28:36.583 回答
6

优点

  • 您可以访问私有成员来测试它们
  • 它的数量相当少hack

缺点

  • 封装破损
  • 更复杂、更易碎的封装破损friend
  • test_backdoor通过在生产端混合测试与生产代码
  • 维护问题(就像对测试代码加好友一样,您已经与测试代码建立了极其紧密的耦合)

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

可能的解决方案

  • 使用 Pimpl 习语,将complex代码与私有成员一起放入 pimpl 中,并为 Pimpl 编写测试。Pimpl 可以前向声明为公共成员,允许在单元测试中进行外部实例化。Pimpl 只能由公共成员组成,使其更易于测试
    • 缺点:代码多
    • 缺点:不透明的类型,调试时很难看到里面
  • 只需测试类的公共/受保护接口。测试您的界面布局的合同。
    • 缺点:单元测试很难/不可能以孤立的方式编写。
  • 类似于 Pimpl 解决方案,但创建一个包含complex代码的自由函数。将声明放在私有标头中(不是库公共接口的一部分),然后对其进行测试。
  • 通过朋友打破封装测试方法/夹具
    • 对此的可能变化:声明friend struct test_context;,将您的测试代码放在实现的方法中struct test_context。这样,您就不必为每个测试用例、方法或夹具添加好友。这应该会降低某人破坏好友关系的可能性。
  • 通过模板特化打破封装
于 2012-07-17T04:39:30.807 回答
2

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

它背后的想法不是我的,实现 完全基于 litb 在他的博客上提出和解释的一个技巧(加上这个Sutter 的 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; 
}
于 2012-07-24T00:05:12.547 回答
2

我很抱歉提出这个建议,但是当这些答案中的大多数方法在没有强重构的情况下无法实现时,它对我很有帮助:在文件的标题之前添加您希望访问其私有成员的类,

#define private public

这是邪恶的,但是

  • 不干扰生产代码

  • 不会像朋友/更改访问级别那样破坏封装

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

所以你可以去...

于 2014-09-22T17:59:48.107 回答
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++ 名称修饰不包括私有/公共状态。我被告知在其他系统上这可能不是真的,它不会链接。

于 2012-07-12T18:31:06.760 回答
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();

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

于 2012-07-12T18:50:40.627 回答
1

我使用了一个函数来测试私有类成员,它刚刚被称为 TestInvariant()。

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

它是虚拟的,任何基类在它拥有之前都称为父版本。

这让我可以一直验证类的内部状态,而不会将类的内部暴露给任何人。我进行了非常简单的测试,但没有理由不能进行复杂的测试,甚至可以使用标志等设置打开或关闭。

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

这会有帮助吗?

于 2012-07-20T15:12:22.727 回答
1

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

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

在我看来,模板解决方案在这方面甚至比朋友更糟糕。您声明的模板的主要好处是您不再需要明确地与班级的测试建立朋友关系。我认为,相反,这是有害的。有两个原因。

  1. 该测试与您的班级内部耦合。任何更改类的人都应该知道,通过更改对象的私有信息,他们可能会破坏测试。朋友确切地告诉他们哪些对象可能与您的类的内部状态耦合,但模板解决方案没有。

  2. 朋友限制了您的私人范围扩展。如果您将某个班级加为好友,您就会知道只有该班级可以访问您的内部信息。因此,如果您对测试加为好友,您就会知道只有测试可以读取或写入私有成员变量。但是,您的模板后门可以在任何地方使用。

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

如果您必须从测试中访问私有成员,只需将测试夹具加为好友并完成它。它简单易懂。

于 2012-07-21T09:08:09.470 回答
0

有一种理论认为,如果它是私有的,则不应单独对其进行测试,如果需要,则应对其进行重新设计。

对我来说,这就是什叶派。

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

class Something{
   PRIVATE:
       int m_attr;
};

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

于 2012-07-20T17:50:16.420 回答