pImpl 习惯用法和可测试性
c++ 中的 pImpl 习惯用法旨在向该类的用户隐藏该类的实现细节(=私有成员)。 然而,它还隐藏了该类的一些依赖项,从测试的角度来看,这些依赖项通常被认为是不好的。
例如,如果类 A 将其实现细节隐藏在只能从 A.cpp 访问的类 AImpl 中,并且 AImpl 依赖于许多其他类,则对类 A 进行单元测试将变得非常困难,因为测试框架无法访问AImpl 也没有办法将依赖注入到 AImpl 中。
以前有人遇到过这个问题吗?你找到解决办法了吗?
- 编辑 -
在一个相关主题上,似乎人们建议只测试接口公开的公共方法,而不是内部方法。虽然我可以从概念上理解该声明,但我经常发现我需要单独测试私有方法。例如,当公共方法调用包含一些重要逻辑的私有帮助器方法时。
The pImpl idiom in c++ aims to hide the implementation details (=private members) of a class from the users of that class.
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.
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.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
pimpl 背后的想法并不是从类中隐藏实现细节(私有成员已经这样做了),而是将实现细节移出标头。问题在于,在 C++ 的包含模型中,更改私有方法/变量将强制重新编译包含此文件的任何文件。这是一种痛苦,这就是 pimpl 寻求消除的原因。它无助于防止对外部库的依赖。其他技术可以做到这一点。
您的单元测试不应依赖于类的实现。他们应该验证您的类是否确实按其应有的方式运行。唯一真正重要的是对象如何与外界交互。您的测试无法检测到的任何行为都必须是对象的内部行为,因此是无关的。
话虽如此,如果您发现类的内部实现过于复杂,您可能希望将该逻辑分解为单独的对象或函数。本质上,如果您的内部行为太复杂而无法间接测试,请将其设为另一个对象的外部行为并对其进行测试。
例如,假设我有一个类,它采用字符串作为其构造函数的参数。该字符串实际上是一种小型语言,它指定了对象的一些行为。 (该字符串可能来自配置文件或其他内容)。理论上,我应该能够通过构造不同的对象并检查行为来测试该字符串的解析。但如果迷你语言足够复杂,这就会很困难。因此,我定义了另一个函数,它接受字符串并返回上下文的表示(如关联数组或其他东西)。然后我可以与主对象分开测试该解析函数。
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. 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. That is a pain, and that's why pimpl seeks to eliminate. 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.
为什么单元测试需要访问 A 实现的内部?
单元测试应该测试A,因此应该只直接关心A的输入和输出。如果某些东西在 A 的接口中不可见(直接或间接),那么它实际上可能根本不需要成为 Aimpl 的一部分(因为它的结果对外部世界不可见)。
如果 Aimpl 产生您需要测试的副作用,则表明您应该检查您的设计。
Why does the unit test need access to the internals of A's implementation?
The unit test should be testing A, and as such should only care about input and output of A directly. 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).
If Aimpl generates side effects you need to test, that indicates you should be taking a look at your design.
如果您正确执行依赖项注入,则任何依赖项类 A as 都应该通过其公共接口传入 - 如果您的 pImpl 由于依赖项而干扰您的测试,那么您似乎没有注入那些依赖关系。
单元测试应该只关注 A 类公开的公共接口; A 对依赖项内部做了什么不是你关心的。只要正确注入所有内容,您就应该能够传入模拟,而无需担心 A 的内部实现。从某种意义上说,您可以说可测试性和正确的 pImpl 是齐头并进的,因为不可测试的实现隐藏了不应该隐藏的细节。
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.
Unit testing should only be concerned with the public interface that class A is exposing; what A does internally with the dependencies isn't your concern. 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. 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 习惯使测试变得更加容易。很奇怪的是看到一组关于“不要测试实现”主题的答案来激励在OP之后这么长时间回答。
通常,基于非 pimpl 的 C++ 中,您有一个具有公共和私有字段的类。公共字段很容易测试,私有字段则比较乏味。不过,公共和私有之间的划分很重要,因为它减少了 api 的宽度,并且通常使以后的更改更容易。
当使用这个习语时,有更好的选择。您可以拥有与单个类完全相同的“公共”接口,但现在只有一个私有字段包含某种指针,例如
my_things_real 类预计在与外部析构函数相同的源文件中可见可见类,但不在标题中。它不是公共接口的一部分,因此所有字段都可以是公共的。
然后针对真实的类编写单元测试。根据您的喜好进行尽可能多或尽可能少的测试。我特意将其称为“real”而不是“impl”,以帮助确保它不会被误认为仅仅是实现细节。
测试这个类非常容易,因为所有字段都是公共的。外部接口非常小,因为它是由其他类定义的。薄薄的翻译层很难出错,但仍然欢迎您通过外部 API 进行测试。这明显是接口和实现分离的明显胜利。
在一个隐约相关的注释中,令我感到荒谬的是,如此多的其他一致的人主张跳过对无法通过外部 API 轻松访问的任何内容的单元测试。最低层的函数很难免受程序员错误的影响。验证 API 是否可用的测试对于验证实现细节是否正确既重要又正交。
The pImpl idiom makes testing far easier. 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.
In usual, non-pimpl based C++ you have a class with public and private fields. 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.
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, e.g.
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. It isn't part of the public interface, so all the fields can be public.
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.
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. 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. 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.
单元测试应该让实现类通过其步伐。一旦 PIMPL 类出现在图中,您就已经进入“集成”领域 - 因此 U/T 不适用。 PIMPL 就是隐藏实现 - 您不应该知道实现的类设置。
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 is all about hiding the implementation - you are not supposed to know the class setup of the implementation.