19

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

例如,如果 A 类将其实现细节隐藏在只能从 A.cpp 访问的 AImpl 类中,并且 AImpl 依赖于许多其他类,那么对 A 类进行单元测试就变得非常困难,因为测试框架无法访问AImpl 也没有办法将依赖项注入到 AImpl 中。

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

- 编辑 -

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

4

5 回答 5

17

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

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

如果 Aimpl 产生了您需要测试的副作用,则表明您应该查看您的设计。

于 2010-05-06T23:38:12.093 回答
17

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

你的单元测试不应该依赖于类的实现。他们应该验证您的班级是否确实按照应有的方式行事。唯一真正重要的是对象如何与外部世界交互。您的测试无法检测到的任何行为都必须是对象内部的,因此是不相关的。

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

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

于 2010-05-07T00:40:46.837 回答
11

如果您正确地进行依赖注入,则任何依赖类 A 都应该通过其公共接口传入 - 如果您的 pImpl 由于依赖关系而干扰您的测试,那么您似乎没有注入这些依赖关系。

单元测试应该只关注 A 类公开的公共接口;A 在内部对依赖项做什么不是您关心的问题。只要一切都被正确注入,您应该能够传入模拟,而无需担心 A 的内部实现。从某种意义上说,您可以说可测试性和适当的 pImpl 齐头并进,因为不可测试的实现隐藏了不应该隐藏的细节。

于 2010-05-06T23:43:22.713 回答
6

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

然后针对真实类编写单元测试。尽可能多地或尽可能少地测试它。我故意将其称为“真实”而不是“实现”,以帮助确保它不会被误认为仅仅是实现细节。

测试这个类非常容易,因为所有字段都是公开的。外部接口非常小,因为它是由另一个类定义的。极薄的翻译层很难出错,但仍然欢迎您通过外部 api 进行测试。从更显着地分离接口和实现来看,这是一个明显的胜利。

在一个模糊的相关说明中,让我觉得很荒谬的是,有这么多原本连贯的人主张跳过对任何无法通过外部 API 轻松访问的东西的单元测试。最低级别的函数几乎不受程序员错误的影响。测试以验证 api 是否可用对于验证实现细节是否正确既重要又正交。

于 2015-12-02T23:18:53.670 回答
0

单元测试应该让实现类通过它的步伐。一旦 PIMPL 类出现在图片中,您就已经进入了“集成”领域——因此 U/T 并不适用。PIMPL 就是隐藏实现——你不应该知道实现的类设置。

于 2015-02-03T21:34:53.887 回答