简体   繁体   English

pImpl成语和可测试性

[英]The pImpl idiom and Testability

The pImpl idiom in c++ aims to hide the implementation details (=private members) of a class from the users of that class. c ++中的pImpl惯用法旨在从该类的用户隐藏类的实现细节(=私有成员)。 However it also hides some of the dependencies of that class which is usually regarded bad from a testing point of view. 但是它也隐藏了该类的一些依赖关系,从测试的角度来看,这通常被认为是不好的。

For example if class A hides its implementation details in Class AImpl which is only accessible from A.cpp and AImpl depends on a lot of other classes, it becomes very difficult to unit test class A since the testing framework has no access to the methods of AImpl and also no way to inject dependency into AImpl. 例如,如果类A隐藏其类AImpl中的实现细节,只能从A.cpp访问,并且AImpl依赖于许多其他类,则单元测试类A变得非常困难,因为测试框架无法访问AImpl也没办法将依赖注入AImpl。

Has anyone come across this problem before? 有没有人遇到过这个问题? and have you found a solution? 你找到了解决方案吗?

-- edit -- - 编辑 -

On a related topic, it seems that people suggest one should only test public methods exposed by the interface and not the internals. 在一个相关的主题上,似乎人们建议应该只测试由接口而不是内部公开的公共方法。 While I can conceptually understand that statement, I often find that I need to test private methods in isolation. 虽然我可以在概念上理解该语句,但我经常发现我需要单独测试私有方法。 For example when a public method calls a private helper method that contains some non trivial logic. 例如,当公共方法调用包含一些非平凡逻辑的私有帮助器方法时。

Why does the unit test need access to the internals of A's implementation? 为什么单元测试需要访问A实现的内部?

The unit test should be testing A, and as such should only care about input and output of A directly. 单元测试应该是测试A,因此应该只关心A的输入和输出。 If something isn't visible in A's interface (either directly or indirectly) then it may not actually need to be part of Aimpl at all (as its results aren't visible to the external world). 如果在A的界面中(直接或间接)看不到某些东西,那么它实际上可能根本不需要成为Aimpl的一部分(因为它的结果对于外部世界是不可见的)。

If Aimpl generates side effects you need to test, that indicates you should be taking a look at your design. 如果Aimpl产生需要测试的副作用,那表明你应该看一下你的设计。

The idea behind pimpl is to not so much to hide implementation details from classes, (private members already do that) but to move implementation details out of the header. pimpl背后的想法不是隐藏类的实现细节,(私有成员已经这样做了),而是将实现细节移出标题。 The problem is that in C++'s model of includes, changing the private methods/variables will force any file including this file to be recompiled. 问题是在C ++的包含模型中,更改私有方法/变量将强制重新编译包含此文件的任何文件。 That is a pain, and that's why pimpl seeks to eliminate. 这是一种痛苦,这就是为什么pimpl试图消除的原因。 It doesn't help with preventing dependencies on external libraries. 它无助于防止对外部库的依赖。 Other techniques do that. 其他技术也是如此。

Your unit tests shouldn't depend on the implementation of the class. 您的单元测试不应该依赖于类的实现。 They should verify that you class actually acts as it should. 他们应该验证你的班级实际上是应该做的。 The only thing that really matter is how the object interacts with the outside world. 唯一真正重要的是物体如何与外界相互作用。 Any behavior which your tests cannot detect must be internal to the object and thus irrelevant. 您的测试无法检测到的任何行为必须是对象的内部因素,因此无关紧要。

Having said that, if you find too much complexity inside the internal implementation of a class, you may want to break out that logic into a separate object or function. 话虽如此,如果在类的内部实现中发现太多复杂性,您可能希望将该逻辑分解为单独的对象或函数。 Essentially, if your internal behavior is too complex to test indirectly, make it the external behavior of another object and test that. 基本上,如果您的内部行为过于复杂而无法间接测试,请将其作为另一个对象的外部行为并对其进行测试。

For example, suppose that I have a class which takes a string as a parameter to its constructor. 例如,假设我有一个类,它将一个字符串作为其构造函数的参数。 The string is actual a little mini-language that specifies some of the behavior the object. 该字符串实际上是一个小的语言,它指定了对象的一些行为。 (The string probably comes from a configuration file or something). (字符串可能来自配置文件或其他东西)。 In theory, I should be able to test the parsing of that string by constructing different objects and checking behavior. 从理论上讲,我应该能够通过构造不同的对象和检查行为来测试该字符串的解析。 But if the mini-language is complex enough this will be hard. 但如果迷你语言足够复杂,那将很难。 So, I define another function that takes the string and returns a representation of the context (like an associative array or something). 所以,我定义另一个函数,它接受字符串并返回上下文的表示(如关联数组或其他东西)。 Then I can test that parsing function separately from the main object. 然后我可以与主对象分开测试该解析功能。

If you're doing dependency injection right, any dependencies class A as should be being passed in through its public interface - if your pImpl is interfering with your testing because of dependencies, it would seem that you're not injecting those dependencies. 如果您正在进行依赖注入,那么任何依赖类A都应该通过其公共接口传入 - 如果您的pImpl由于依赖性而干扰您的测试,那么您似乎并没有注入这些依赖项。

Unit testing should only be concerned with the public interface that class A is exposing; 单元测试应该只关注A类暴露的公共接口; what A does internally with the dependencies isn't your concern. A 内部的依赖关系不是你的问题。 As long as everything is being injected properly, you should be able to pass in mocks without needing to worry about A's internal implementation. 只要正确注入所有内容,您就应该能够传入模拟而无需担心A的内部实现。 In a sense, you could say testability and proper pImpl go hand-in-hand, in that a non-testable implementation is hiding details that shouldn't be hidden. 从某种意义上说,你可以说可测试性和适当的pImpl是相辅相成的,因为不可测试的实现隐藏了不应隐藏的细节。

The pImpl idiom makes testing far easier. pImpl成语使测试变得更加容易。 It's strange enough to see a set of answers on the theme of "don't test the implementation" to motivate answering so long after the OP. 很遗憾看到一套关于“不测试实施”主题的答案,以便在OP后很长时间内激发回答。

In usual, non-pimpl based C++ you have a class with public and private fields. 在通常的非基于pimpl的C ++中,您有一个包含公共和私有字段的类。 Public fields are easy to test, private fields somewhat more tedious. 公共领域很容易测试,私有领域有点繁琐。 The division between public and private is important though, since it decreases the width of the api and usually makes later changes easier. 公共和私有之间的划分很重要,因为它减少了api的宽度,并且通常使后来的更改更容易。

When using this idiom a better option is available. 使用这个习语时,可以选择更好的选项。 You can have exactly the same "public" interface as with a single class, but now there's only one private field containing a pointer of some sort, eg 你可以拥有与单个类完全相同的“公共”接口,但现在只有一个私有字段包含某种类型的指针,例如

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

The my_things_real class is expected to be visible in the same source file as the destructor of the externally visible class, but not in the header. my_things_real类应该在与外部可见类的析构函数相同的源文件中可见,但不在标题中。 It isn't part of the public interface, so all the fields can be public. 它不是公共接口的一部分,因此所有字段都可以是公共的。

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

Unit tests are then written against the real class. 然后针对真实类别编写单元测试。 Test as much or as little of it as you like. 尽可能多地测试它。 I've deliberately called it "real" instead of "impl" to help ensure that it isn't mistaken for a mere implementation detail. 我故意称它为“真实的”而不是“impl”,以帮助确保它不会被误认为只是一个实现细节。

Testing this class is very easy since all the fields are public. 由于所有字段都是公共的,因此测试此类非常简单。 The external interface is very small since it's defined by the other class. 外部接口非常小,因为它是由另一个类定义的。 The wafer-thin translation layer is difficult to get wrong, but you're still welcome to test through the external api as well. 晶圆薄的转换层很难出错,但您仍然可以通过外部api进行测试。 This is a clear win from more significantly separating interface and implementation. 这是从更明显地分离界面和实现的明显胜利。

On a vaguely related note, it strikes me as absurd that so many otherwise coherent people advocate skipping unit testing for anything that is not readily accessible through the external API. 在一个含糊不清的相关说明中,令我觉得荒谬的是,如此多的其他连贯的人主张跳过单元测试,以查找通过外部API无法轻易访问的任何内容。 The lowest level functions are hardly immune to programmer errors. 最低级别的功能几乎不受程序员错误的影响。 Testing to verify that the api is usable is both important and orthogonal to verifying that the implementation details are correct. 验证api是否可用的测试对于验证实现细节是否正确非常重要且正交。

The unit testing should put the implementation class thru its paces. 单元测试应该使实现类达到它的步伐。 Once the PIMPL class is in the picture, you are already into the realm of "integration" - and hence U/T does not apply as such. 一旦PIMPL类出现在图片中,您已进入“集成”领域 - 因此U / T不适用于此类。 PIMPL is all about hiding the implementation - you are not supposed to know the class setup of the implementation. PIMPL是关于隐藏实现的 - 你不应该知道实现的类设置。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM