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

时间:2018-09-03 15:30:03

标签: c++ unit-testing googletest googlemock

我尝试为我们的C ++旧代码库实现uni测试。我通读了Michael Feathers的“有效地使用遗留代码”,并且对如何实现我的目标有所了解。我使用GooleTest / GooleMock作为框架,并且已经实现了一些涉及模拟对象的测试。

为此,我尝试了“提取接口”方法,这种方法在一种情况下效果很好:

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

成为:

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

,我在生产中通过了ProdMyOtherClass,在测试中通过了MockMyOtherClass。到目前为止一切都很好。

但是现在,我有另一个使用MyClass的类,例如:

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

如果我想测试WorkOnMyClass,并且想在测试期间模拟MyClass,则必须再次提取接口。这就引出了我的问题,到目前为止我还找不到答案:接口的外观如何?我的猜测是,它应该都是抽象的,所以:

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

这给我每个类三个文件:所有虚拟基本接口类,使用所有生产参数的生产实现和使用所有模拟参数的模拟实现。这是正确的方法吗?

我只找到了简单的示例,其中函数参数是基元,而不是类,而类又需要本身进行测试(因此可能需要接口)。

2 个答案:

答案 0 :(得分:5)

粗体TLDR

正如杰弗里·科芬(Jeffery Coffin)所指出的那样,没有一种正确的方法来完成您要完成的任务。 软件中没有“一刀切”的解决方案,因此请一口气拿出所有这些答案,并对您的项目使用您的最佳判断, 话虽如此,这是一个潜在的替代方法:

当心嘲笑地狱:

您概述的方法将起作用:但这可能不是最好的方法(或者可能只有您可以决定)。 通常,您倾向于使用模拟程序的原因是因为您希望打破某些依赖关系。提取接口是一个不错的模式,但是它可能无法解决核心问题。过去,我在很大程度上依赖于模拟,在某些情况下我对此感到非常遗憾。它们有它们的位置,但是我尝试尽可能少地使用它们,并且使用最低级别最小可能的类。 您可以进入模拟地狱,因为您必须推理模拟物具有模拟物,所以您将要进入该模拟地狱。通常,发生这种情况是因为存在继承/组成结构,并且基级/子级共享一个依赖项。如果可能,您希望进行重构,以使该依赖项不会在您的类中根深蒂固。

隔离“真实”依赖项:

更好的模式可能是Parameterize Constructor(另一个Michael Feathers WEWLC模式)。

WLOG,可以说您的流氓依赖项是一个数据库(也许它不是数据库,但是这个想法仍然成立)。也许MyClassMyOtherClass都需要访问它。 而不是为这些类的两者提取接口,而是尝试隔离依赖性并将其传递给每个类的构造函数。

示例:

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

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

现在我们看到了这种布局,这使我们意识到您可能甚至希望MyOtherClass成为成员MyClass中的>(取决于您在做什么以及它们之间的关系)。这样可以避免实例化MyOtherClass时出错,并减轻依赖关系所有权的负担。

另一种替代方法是使Database为单例,以减轻所有权负担。这对于Database来说效果很好,但是一般而言,单例模式不能满足所有依赖关系。

优点:

  • 允许进行干净的(标准)依赖项注入,并且解决了隔离真正依赖项的核心问题。
  • 隔离真正的依赖关系使其可以避免嘲弄地狱,而只需传递依赖关系即可。
  • 更好的面向未来的设计,图案的高可重用性,并且可能不太复杂。下一个需要依赖项的类不必模拟自己,而只需将依赖项作为参数绑定即可。

缺点:

  • 与提取接口相比,此模式可能需要更多的时间/精力。在旧版系统中,有时这种情况不可行。我犯了种种罪过,因为昨天我们需要删除某个功能。没关系,它发生了。只需注意您累积的设计陷阱和技术欠债...
  • 这也容易出错。

我使用了一些通用的旧提示(WEWLC不会告诉您的事情):

如果您不需要避免依赖,请避免避免依赖。。在使用重构的遗留系统时尤其如此。冒险一般。相反,您可以让测试调用 actual 数据库(或任何依赖项),但是让测试套件连接到小型“ test”数据库而不是“ prod”数据库。建立一个小的测试数据库的成本通常很小。由于嘲笑模拟或模拟与现实不同步而使产品崩溃的成本通常要高得多。这还会为您节省很多编码。

在可能的情况下避免进行模拟(尤其是繁重的模拟)。随着我逐渐成为一名软件工程师,我越来越相信模拟是一种迷你设计。它们既快速又肮脏:但是通常说明了一个更大的问题。

构想理想的API,并尝试构建您构想的东西。您不能实际上构筑理想的API,但想像一下您可以立即重构所有内容并拥有您想要的API。这是改进遗留系统的好起点,并且可以随时进行当前设计/实现的权衡/牺牲。

HTH,祝你好运!

答案 1 :(得分:4)

要记住的第一点是,可能没有一种方法是对的,而另一种方法是错误的。任何答案都是与事实同等的意见问题(尽管意见可以由事实告知)。

也就是说,对于这种情况,我强烈建议至少谨慎使用继承。大多数此类书籍/作者都非常注重Java,在Java中,继承被视为瑞士军刀(或可能是莱瑟曼)的技术,用于可能有点接近意义的每项任务,无论其是否合理。真正适合该工作的工具。在C ++中,继承趋向于被狭义地看待,仅在几乎没有其他选择的情况下(/选择/无论如何手动滚动本质上继承的东西)使用继承。

继承的主要独特特征是运行时多态性。例如,我们有一个(指向)对象的集合,并且集合中的对象不是全部相同的类型(但都是通过继承关联的)。我们使用虚函数为各种类型的对象提供通用接口。

至少在我读东西的时候,这里根本不是这样。在给定的版本中,您将处理 模拟对象生产对象,但是您始终会在编译时知道使用的对象是模拟对象还是生产对象- -您将永远不会有模拟对象和生产对象的混合集合,并且需要在运行时确定特定对象是模拟对象还是生产对象。

假设是正确的,继承几乎可以肯定是该工作的错误工具。当您处理静态多态性时(即,行为是在编译时确定的),有更好的工具(尽管Feather和company显然不得不忽略它们,仅仅是因为Java无法提供它们)。

在这种情况下,在构建时处理所有工作非常简单,而不会以任何额外的复杂性污染您的生产代码。举一个例子,您可以创建带有mockproduction子目录的源目录。在mock目录中,您有foo.cppbar.cppbaz.cpp实现了类FooBar和{{1}的模拟版本} 分别。在生产目录中,您具有相同的生产版本。在构建时,您告诉构建工具是生成生产版本还是模拟版本,然后它会基于该目录选择获取源代码的目录。

除了半不相关

我还注意到您正在使用Baz作为参数。这是另一个巨大的危险信号。我发现shared_ptr的使用极为罕见。我见过的 vast 大多数时候,它并不是真正应该使用的东西。 shared_ptr用于对象共享所有权的情况-但大多数用法似乎更接近“我没有费心找出该对象的所有权”。 shared_ptr本身并不是什么大问题,但通常是更大问题的征兆。