【问题标题】:The pImpl idiom and TestabilitypImpl 习语和可测试性
【发布时间】:2010-05-06 23:29:43
【问题描述】:

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

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

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

-- 编辑--

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

【问题讨论】:

  • 为了响应您的编辑,如果您的“私有”功能足够复杂,需要独立测试,那么您应该将其放入您独立测试的自己的模块中。

标签: c++ testing pimpl-idiom


【解决方案1】:

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

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

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

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

【讨论】:

  • 实现独立测试对于黑盒测试来说是可以的。但在白盒测试中,我想测试实现细节。因此,访问类/对象的内部很重要。
  • 您可以将“私有”函数/类放在未命名的命名空间中。然后#include源文件直接在你的测试代码中。如果您想避免创建头文件只是为了测试某些东西,这很有效。这个想法在google-test docs中有提到
【解决方案2】:

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

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

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

【讨论】:

  • 同意。您测试的是接口,而不是内部。
  • 我想我目前在其他技术可能更适合的地方使用 pImpl。更具体地说,我使用 pImpl 来隐藏将来可能被替换的外部库的使用。我认为这是一个实现细节,并认为 pImpl 很合适,但我想一定有更好的方法来做到这一点。 (例如接口、适配器、策略等)但这让我很困惑..你能给我一个例子说明什么时候 pImpl 比这些模式更适合吗?
  • @Rimo,pImplpimpl 的重点,阅读:en.wikipedia.org/wiki/… 是为了隐藏实现。从测试的角度来看,您需要测试x 输入是否产生预期的y 输出。这是如何实现的,这是一个实现细节,而不是您的测试责任(从单元测试的角度来看)。
  • +1 测试规范 - 不是你对课程的理解。
  • 实现独立测试对于黑盒测试来说是可以的。但在白盒测试中,我想测试实现细节。因此,访问类/对象的内部很重要。
【解决方案3】:

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

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

【讨论】:

  • 您是说只需要测试 A 类的公共方法。但是您有时不需要单独对私有辅助方法进行单元测试吗?例如,谷歌测试框架特别支持 FRIEND_TEST 宏。
  • @Rimo:你能用你用来测试非pimpl类的私有方法的技术来测试pimpl类吗?还是我错过了什么?
【解决方案4】:

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 是否可用的测试对于验证实现细节是否正确既重要又正交。

【讨论】:

  • 请注意 my_things::my_things_real 如何对 my_things 私有,想知道测试将如何使用它:)
  • 因为 my_things::my_things_real 是在 .cpp 源文件中定义的,您如何将其#include 到测试文件中?
  • 它要么在头文件中定义,要么测试在同一个源文件中。除了我以外的人似乎更喜欢前者。当代码库是我自己的时,我会在他们正在测试的东西旁边编写测试。
【解决方案5】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-12
    • 2020-09-05
    相关资源
    最近更新 更多