pImpl成语和可测试性

时间:2010-05-06 23:29:43

标签: c++ testing pimpl-idiom

c ++中的pImpl惯用法旨在从该类的用户隐藏类的实现细节(=私有成员)。 然而,它也隐藏了该类的一些依赖性,从测试的角度来看,这通常被认为是坏的。

例如,如果A类隐藏其类AImpl中的实现细节,而AImpl只能从A.cpp访问而AImpl依赖于许多其他类,则由于测试框架无法访问,因此单元测试A类变得非常困难。 AImpl的方法也无法将依赖注入AImpl。

以前有人遇到过这个问题吗?你找到了解决方案吗?

- 编辑 -

在一个相关的主题上,似乎人们建议应该只测试由接口而不是内部公开的公共方法。虽然我可以在概念上理解该语句,但我经常发现我需要单独测试私有方法。例如,当公共方法调用包含一些非平凡逻辑的私有帮助器方法时。

5 个答案:

答案 0 :(得分:17)

为什么单元测试需要访问A实现的内部?

单元测试应该是测试A,因此应该只关心A的输入和输出。如果在A的界面中(直接或间接)看不到某些东西,那么它实际上可能根本不需要成为Aimpl的一部分(因为它的结果对于外部世界是不可见的)。

如果Aimpl产生需要测试的副作用,那表明你应该看看你的设计。

答案 1 :(得分:16)

pimpl背后的想法不是隐藏类的实现细节,(私有成员已经这样做了),而是将实现细节移出标题。问题是在C ++的包含模型中,更改私有方法/变量将强制重新编译包含此文件的任何文件。这是一种痛苦,这就是为什么pimpl试图消除的原因。它无助于防止对外部库的依赖。其他技术可以做到这一点。

您的单元测试不应取决于课程的实施。他们应该验证你的班级实际上是应该做的。唯一真正重要的是物体如何与外界相互作用。您的测试无法检测到的任何行为必须是对象的内部因素,因此无关紧要。

话虽如此,如果您在类的内部实现中发现太多复杂性,您可能希望将该逻辑分解为单独的对象或函数。基本上,如果您的内部行为太复杂而无法间接测试,请将其作为另一个对象的外部行为并进行测试。

例如,假设我有一个类,它将一个字符串作为其构造函数的参数。该字符串实际上是一个小的语言,它指定了对象的一些行为。 (字符串可能来自配置文件或其他东西)。从理论上讲,我应该能够通过构造不同的对象和检查行为来测试该字符串的解析。但如果迷你语言足够复杂,那将很难。所以,我定义另一个函数,它接受字符串并返回上下文的表示(如关联数组或其他东西)。然后我可以与主对象分开测试该解析函数。

答案 2 :(得分:11)

如果你正在进行依赖注入,那么任何依赖类A应该通过它的公共接口传入 - 如果你的pImpl由于依赖性而干扰你的测试,那么你似乎不是注入这些依赖项。

单元测试应该只关注A类暴露的公共接口; A 内部与依赖关系的内容不是您关注的问题。只要正确注入所有内容,您就应该能够传入模拟而无需担心A的内部实现。从某种意义上说,你可以说可测试性和适当的pImpl是相辅相成的,因为不可测试的实现隐藏了不应该隐藏的细节。

答案 3 :(得分:3)

pImpl成语使测试变得更加容易。很遗憾看到一套关于“不测试实施”主题的答案,以便在OP之后很长时间内激发回答。

在通常的非基于pimpl的C ++中,你有一个包含公共和私有字段的类。公共领域很容易测试,私有领域有点繁琐。公共和私人之间的划分很重要,因为它减少了api的宽度,并且通常使后来的更改更容易。

使用这个习语时,可以选择更好的选项。你可以拥有与单个类完全相同的“公共”接口,但现在只有一个私有字段包含某种类型的指针,例如。

class my_things
{
  public:
    my_things();
    ~my_things();
    void do_something_important(int);
    int also_this();
  private:
    struct my_things_real;
    std::unique_ptr<my_things_real> state;
};

my_things_real类应该在与外部可见类的析构函数相同的源文件中可见,但不在标题中。它不是公共接口的一部分,因此所有字段都可以公开。

void my_things::do_something_important(int x) { state->doit(x); } // etc

class my_things_real // I'd probably write 'struct'
{
  public:
    int value;
    void doit(int x) { value = x; }
    int getit() { return value; }
};

然后针对真实类别编写单元测试。尽可能多地测试它。我故意称它为“真实的”而不是“impl”,以帮助确保它不会被误认为只是一个实现细节。

测试此类非常简单,因为所有字段都是公共的。外部接口非常小,因为它是由另一个类定义的。晶圆薄的转换层很难出错,但您仍然可以通过外部api进行测试。这是从更明显地分离界面和实现的明显胜利。

在一个含糊不清的相关说明中,让我觉得荒谬的是,如此多的其他连贯的人主张跳过单元测试以寻找任何无法通过外部API访问的内容。最低级别的功能几乎不受程序员错误的影响。测试验证api是否可用对于验证实现细节是否正确非常重要和正交。

答案 4 :(得分:0)

单元测试应该将实现类放到它的步伐。一旦PIMPL类出现在图片中,您就已经进入了&#34;整合&#34; - 因此U / T不适用。 PIMPL就是隐藏实现 - 你不应该知道实现的类设置。