简体   繁体   English

在C ++单元测试上下文中,抽象基类是否应该将其他抽象基类作为函数参数?

[英]In a C++ unit test context, should an abstract base class have other abstract base classes as function parameters?

I try to implement uni tests for our C++ legacy code base. 我尝试为我们的C ++遗留代码库实现uni测试。 I read through Michael Feathers "Working effectively with legacy code" and got some idea how to achieve my goal. 我通过Michael Feathers“有效地使用遗留代码”阅读并了解了如何实现我的目标。 I use GooleTest/GooleMock as a framework and already implemented some first tests involving mock objects. 我使用GooleTest / GooleMock作为框架,并且已经实现了一些涉及模拟对象的首次测试。

To do that, I tried the "Extract interface" approach, which worked quite well in one case: 为此,我尝试了“提取界面”方法,在一种情况下效果很好:

class MyClass
{
  ...
  void MyFunction(std::shared_ptr<MyOtherClass> parameter);
}

became: 成为:

class MyClass
{
  ...
  void MyFunction(std::shared_ptr<IMyOtherClass> parameter);
}

and I passed a ProdMyOtherClass in production and a MockMyOtherClass in test. 我通过了ProdMyOtherClass生产和MockMyOtherClass测试。 All good so far. 到目前为止都很好。

But now, I have another class using MyClass like: 但现在,我有另一个使用MyClass类,如:

class WorkOnMyClass
{
  ...
  void DoSomeWork(std::shared_ptr<MyClass> parameter);
}

If I want to test WorkOnMyClass and I want to mock MyClass during that test, I have to extract interface again. 如果我想测试WorkOnMyClass并且我想在测试期间模拟MyClass ,我必须再次提取接口。 And that leads to my question, which I couldn't find an answer to so far: how would the interface look like? 这导致了我的问题,到目前为止我找不到答案:界面怎么样? My guess is, that it should be all abstract, so: 我的猜测是,它应该是抽象的,所以:

class IMyClass
{
  ...
  virtual void MyFunction(std::shared_ptr<IMyOtherClass> parameter) = 0;
}

That leaves me with three files for every class: all virtual base interface class, production implementation using all production parameters and mock implementation using all mock parameters. 这给我留下了每个类的三个文件:所有虚拟基础接口类,使用所有生产参数的生产实现和使用所有模拟参数的模拟实现。 Is this the correct approach? 这是正确的方法吗?

I only found simple examples, where function parameters are primitives, but not classes, which in turn need tests themselves (and may therefore require interfaces). 我只找到了简单的例子,其中函数参数是基元,而不是类,而这些类本身又需要测试(因此可能需要接口)。

TLDR in bold TLDR以粗体显示

As Jeffery Coffin has already pointed out, there is no one right way to do what you're seeking to accomplish. 正如Jeffery Coffin已经指出的那样,没有一种正确的方法可以做你想要完成的事情。 There is no "one-size fits all" in software, so take all these answers with a grain of salt, and use your best judgement for your project and circumstances. 在软件中没有“一刀切”的所有内容,因此请将所有这些答案放在一边,并根据 项目和情况使用您的最佳判断。 That being said, here's one potential alternative: 话虽如此,这是一个潜在的替代方案:

Beware of mocking hell: 小心嘲笑地狱:

The approach you've outlined will work: but it might not be best (or it might be, only you can decide). 您概述的方法将起作用:但它可能不是最好的(或者可能是,只有您可以决定)。 Typically the reason you're tempted to use mocks is because there's some dependency you're looking to break. 通常情况下,您想要使用模拟的原因是因为您需要打破一些依赖性。 Extract Interface is an okay pattern, but it's probably not resolving the core issue. Extract Interface是一个好的模式,但它可能无法解决核心问题。 I've leaned heavily on mocks in the past and have had situations where I really regret it. 我过去常常倾向于嘲笑,并且有过我真的后悔的情况。 They have their place, but I try to use them as infrequently as possible, and with the lowest-level and smallest possible class. 他们有自己的位置,但我尽量不经常使用它们,并且尽可能使用最低级别最小级别。 You can get into mocking hell, which you're about to enter since you have to reason about your mocks having mocks. 你可以进入嘲讽地狱,因为你不得不推断你的嘲笑有嘲笑。 Usually when this happens its because there's a inheritance/composition structure and the base/children share a dependency. 通常当发生这种情况时,因为存在继承/组合结构,并且base / children共享依赖关系。 If possible, you want to refactor so that the dependency isn't so heavily ingrained in your classes. 如果可能的话,您希望重构,以便依赖关系不会在您的类中根深蒂固。

Isolating the "real" dependency: 隔离“真正的”依赖:

A better pattern might be Parameterize Constructor (another Michael Feathers WEWLC pattern). 更好的模式可能是Parameterize Constructor(另一个Michael Feathers WEWLC模式)。

WLOG, lets say your rogue dependency is a database (maybe it's not a database, but the idea still holds). WLOG,让我们说你的流氓依赖是一个数据库(也许它不是一个数据库,但这个想法仍然存在)。 Maybe MyClass and MyOtherClass both need access to it. 也许MyClassMyOtherClass都需要访问它。 Instead of Extracting Interface for both of these classes, try to isolate the dependency and pass it in to the constructors for each class. 而不是为这两个类提取接口, 尝试隔离依赖项并将其传递给每个类的构造函数。

Example: 例:

class MyClass {
public:
    MyClass(...) : ..., db(new ProdDatabase()) {}; // Old constructor, but give it a "default" database now
    MyClass(..., Database* db) : ..., db(db) {}; // New constructor
    ...
private:
    Database* db; // Decide on semantics about owning a database object, maybe you want to have the destructor of this class handle it, or maybe not
    // MyOtherClass* moc; //Maybe, depends on what you're trying to do
};

and

class MyOtherClass {
public:
    // similar to above, but you might want to disallow this constructor if it's too risky to have two different dependency objects floating around.
    MyOtherClass(...) : ..., db(new ProdDatabase());
    MyOtherClass(..., Database* db) : ..., db(db);
private:
    Database* db; // Ownership?
};

And now that we see this layout, it makes us realize that you might even want MyOtherClass to simply be a member of MyClass (depends what you're doing and how they're related). 现在我们看到了这种布局,它让我们意识到你甚至可能希望MyOtherClass只是MyClass成员 (取决于你正在做什么以及它们是如何相关的)。 This will avoid mistakes in instantiating MyOtherClass and ease the burden of the dependency ownership. 这样可以避免在实例化MyOtherClass出错,并减轻依赖所有权的负担。

Another alternative is to make the Database a singleton to ease the burden of ownership. 另一种方法是使Database成为单身人士,以减轻所有权负担。 This will work well for a Database , but in general the singleton pattern won't hold for all dependencies. 这对Database很有用,但一般来说,单例模式不适用于所有依赖项。

Pros: 优点:

  • Allows for clean (standard) dependency injection, and it tackles the core issue of isolating the true dependency. 允许干净(标准)依赖注入, 它解决了隔离真正依赖关系的核心问题。
  • Isolating the real dependency makes it so that you avoid mocking hell and can just pass the dependency around. 隔离真正的依赖关系使得你可以避免模仿地狱,并且只能传递依赖关系。
  • Better future proofed design, high reusability of the pattern, and likely less complex. 更好的未来验证设计,模式的高可重用性,并且可能不那么复杂。 The next class that needs the dependency won't have to mock themselves, instead they just rope in the dependency as a parameter. 需要依赖关系的下一个类不必模拟自己,而只是将依赖关系作为参数。

Cons: 缺点:

  • This pattern will probably take more time/effort than Extract Interface. 这种模式可能比Extract Interface花费更多的时间/精力。 In legacy systems, sometimes this doesn't fly. 在传统系统中,有时这不会飞。 I've committed all sorts of sins because we needed to move a feature out...yesterday. 我犯了各种各样的罪,因为我们需要将一个功能移出...昨天。 It's okay, it happens. 没关系,它发生了。 Just be aware of the design gotchas and technical debt you accrue... 只要注意你积累的设计陷阱和技术债务......
  • It's also a bit more error prone. 它也更容易出错。

Some general legacy tips I use (the things WEWLC doesn't tell you): 我使用的一些一般遗留技巧(WEWLC没有告诉你的事情):

Don't get hell-bent about avoiding a dependency if you don't need to avoid it . 如果你不需要避免依赖,就不要害怕避免依赖。 This is especially true when working with legacy systems where refactorings are risky in general. 在使用遗留系统时尤其如此,因为遗留系统的重构通常存在风险。 Instead, you can have your tests call an actual database (or whatever the dependency is), but have the test suite connect to a small "test" database instead of the "prod" database. 相反,您可以让您的测试调用实际数据库(或任何依赖项),但让测试套件连接到一个小的“测试”数据库而不是“prod”数据库。 The cost of standing up a small test db is usually quite small. 站立一个小测试数据库的成本通常很小。 The cost of crashing prod because you goofed up a mock or a mock fell out of sync with reality is typically a lot higher. 因为你嘲笑模拟或模拟与现实失去同步而导致的崩溃成本通常要高得多。 This will also save you a lot of coding. 这也将为您节省大量编码。

Avoid mocks (especially heavy mocking) where possible. 尽可能避免嘲笑(特别是嘲笑)。 I am becoming more and more convinced as I age as a software engineer that mocks are mini-design smells. 随着我作为软件工程师的年龄越来越多,我越来越相信嘲笑是迷你设计的气味。 They are the quick and dirty: but usually illustrate a larger problem. 它们既快又脏:但通常说明一个更大的问题。

Envision the ideal API, and try to build what you envision. 设想理想的API,并尝试构建您想象的。 You can't actually build the ideal API, but imagine you can refactor everything instantly and have the API you desire. 实际上无法构建理想的API,但想象您可以立即重构所有内容并拥有所需的API。 This is a good starting point for improving a legacy system, and make tradeoffs/sacrifices with your current design/implementation as you go. 这是改进遗留系统的良好起点,并随着您的当前设计/实施做出权衡/牺牲。

HTH, good luck! HTH,祝你好运!

The first point to keep in mind is that there probably is no one way that's right and the others wrong--any answer is a matter of opinion as much as fact (though the opinions can be informed by fact). 要记住的第一点是,可能没有一种方法是正确的而其他方式是错误的 - 任何答案都是与事实一样的意见问题(尽管意见可以通过事实得知)。

That said, I'd urge at least a little caution against the use of inheritance for this case. 那就是说,我要求至少对这种情况下使用继承有点谨慎。 Most such books/authors are oriented pretty heavily toward Java, where inheritance is treated as the Swiss army knife (or perhaps Leatherman) of techniques, used for every task where it might sort of come close to making a little sense, regardless of whether its really the right tool for the job or not. 大多数此类书籍/作者都非常注重Java,其中继承被视为瑞士军刀(或者可能是Leatherman)的技术,用于可能有点意义的每一项任务,无论其是否为真的是工作的正确工具。 In C++, inheritance tends to be viewed much more narrowly, used only when/if/where there's nearly no alternative (and the alternative is to hand-roll what's essentially inheritance on your own anyway). 在C ++中,继承往往被更加狭隘地看待,仅在/ if / where几乎没有其他选择时使用(并且替代方案是自己手动滚动本质上的继承)。

The primary unique feature of inheritance is run-time polymorphism. 继承的主要独特功能是运行时多态。 For example, we have a collection of (pointers to) objects, and the objects in the collection aren't all the same type (but are all related via inheritance). 例如,我们有一个(指向)对象的集合,并且集合中的对象不是所有相同的类型(但都通过继承相关)。 We use virtual functions to provide a common interface to the objects of the various types. 我们使用虚函数为各种类型的对象提供通用接口。

At least as I read things, that's not the case here at all though. 至少在我阅读的时候,情况并非如此。 In a given build, you'll deal with either mock objects or production objects, but you'll always know at compile time whether the objects in use are mock or production--you won't ever have a collection of a mixture of mock objects and production objects, and need to determine at run time whether a particular object is mock or production. 在一个给定的版本,您会处理模拟对象生产对象,但你永远知道在编译时使用的对象是否是模拟或生产-你永远不会有假的混合物组成的集合对象和生产对象,需要在运行时确定特定对象是模拟还是生成。

Assuming that's correct, inheritance is almost certainly the wrong tool for the job. 假设这是正确的,继承几乎肯定是工作的错误工具。 When you're dealing with static polymorphism (ie, the behavior is determined at compile time) there are better tools (albeit, ones Feather and company apparentlyy feel obliged to ignore, simply because Java fails to provide them). 当你处理静态多态性时(即,在编译时确定行为),有更好的工具(虽然,Feather和公司显然觉得有必要忽略,仅仅因为Java无法提供它们)。

In this case, it's pretty trivial to handle all the work at build time, without polluting your production code with any extra complexity at all. 在这种情况下,在构建时处理所有工作是非常简单的,而不会以任何额外的复杂性污染您的生产代码。 For one example, you can create a source directory with mock and production subdirectories. 例如,您可以使用mockproduction子目录创建源目录。 In the mock directory you have foo.cpp , bar.cpp and baz.cpp that implement the mock versions of classes Foo , Bar and Baz respectively. mock目录中,你有foo.cppbar.cppbaz.cpp ,它们分别实现类FooBarBaz的模拟版本。 In the production directory you have production versions of the same. 在生产目录中,您有相同的生产版本。 At build time, you tell the build tool whether to build the production or mock version, and it chooses the directory where it gets the source code based on that. 在构建时,您告诉构建工具是构建生产版还是模拟版,并根据它选择获取源代码的目录。

Semi-unrelated aside 半无关

I also note that you're using a shared_ptr as a parameter. 我还注意到您使用shared_ptr作为参数。 This is yet another huge red flag. 这是另一个巨大的红旗。 I find uses for shared_ptr to be exceptionally rare. 我觉得shared_ptr用途非常罕见。 The vast majority of times I've seen it used, it wasn't really what should have been used. 我见过它的绝大部分时间都使用过,实际上并不应该使用它。 A shared_ptr is intended for cases of shared ownership of an object--but most use seems to be closer to "I haven't bothered to figure out the ownership of this object". shared_ptr适用于对象共享所有权的情况 - 但大多数使用似乎更接近“我没有费心去弄清楚这个对象的所有权”。 The shared_ptr isn't all that huge of a problem in itself, but it's usually a symptom of larger problems. shared_ptr本身并不是一个大问题,但它通常是更大问题的症状。

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

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