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

ova*_*nes 18 c++ unit-testing private protected members

今天我与一位同事讨论了是否要测试是否在课堂上测试私人成员或私人状态.他几乎让我相信为什么它有意义.这个问题的目的不是复制已经存在的关于测试私有成员的性质和原因的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_;
};
Run Code Online (Sandbox Code Playgroud)

我不喜欢让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_;
};
Run Code Online (Sandbox Code Playgroud)

通过添加这个函数,我们可以使类的私有成员可测试.注意,不依赖于单元测试类,也不依赖于模板函数实现.在此示例中,单元测试实现使用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);
}
Run Code Online (Sandbox Code Playgroud)

通过引入一个根本不可调用的模板声明,我们为测试实现者提供了将测试逻辑转发到函数中的可能性.该函数作用于类型安全上下文,并且仅在特定测试编译单元内部可见,这是由于测试上下文的匿名类型性质.最好的是,我们可以定义尽可能多的匿名测试上下文,并专门对它们进行测试,而不必触及测试类.

当然,用户必须知道什么是模板专业化,但这段代码真的很糟糕或奇怪或不可读吗?或者我可以期望C++开发人员知道C++模板专业化是什么以及它是如何工作的?

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

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  ...
}
Run Code Online (Sandbox Code Playgroud)

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

我希望看到你的赞成和赞成实践这种方法.

Att*_*ila 21

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

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

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


Zac*_*Zac 6

优点

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

缺点

  • 封装破损
  • 破碎的封装更复杂,同样脆弱 friend
  • 通过投入test_backdoor生产方面将测试与生产代码混合
  • 维护问题(就像为测试代码提供信息一样,您已经创建了与测试代码非常紧密的耦合)

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

可能的解决方案

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


cel*_*vek 3

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

它背后的想法不是我的,其实现 完全基于 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; 
}
Run Code Online (Sandbox Code Playgroud)