用单元测试重新构建C ++应用程序

时间:2015-12-08 14:27:09

标签: c++ unit-testing architecture

我开始考虑重新构建一个大型C ++应用程序,并考虑单元测试。我所做的大部分阅读都引导我去模拟框架(即谷歌模拟)。但是,我的设计目标之一是使软件尽可能简单,以便于维护。

我的问题是,您似乎需要为应用程序添加相当大的复杂性,以便构建使用模拟类所需的依赖注入。

例如,您需要为可能需要模拟的所有类添加抽象基类,以便您可以实例化" production"生产代码中的对象和" mock"单元测试代码中的对象。由于额外类的数量和所有类的增加抽象级别,这有点不合需要。此外,您是否添加了一个定义公共接口的抽象基类到每个类?如果你不这样做,你怎么能确定这个课程永远不需要被嘲笑?

或者,您需要对所有课程进行模板化,以便能够注入"单元测试代码中的模拟对象。我绝对不希望每个类都是模板类的应用程序。

每个人的经历是什么?您如何在您的架构中构建可测试性以及结果如何?

1 个答案:

答案 0 :(得分:3)

  

例如,您需要为所有人添加抽象基类   可能需要模拟的类,以便您可以实例化   "生产"生产代码中的对象和" mock"中的对象   单元测试代码。由于数量的原因,这有点不合需要   额外的类和所有类的增加抽象级别。

我认为在基于模拟的测试框架很有意义之前,首先要了解它的必要性。

例如,就我而言,我已经处理了很多有点大的代码库。它们并不是巨大的,最小的是大约50万行代码,最大的是大约2000万LOC。然而,即使是最小的一个,也可以从软件设计核心的抽象接口中获益匪浅。

抽象中心接口

所有这些代码库的共同点之一是位于其基础核心的软件开发工具包。第三方会使用我们的SDK为我们的产品编写甚至销售插件,他们使用我们用于构建主要产品的相同中央API构建这些插件。

为了能够编写在运行时添加的插件,需要插件依赖于抽象接口,具体实现位于其他地方(例如:在主应用程序二进制文件中或在另一个插件中)。

因此,在我们的案例中,强烈需要软件的核心由抽象接口组成,无论是否有单元测试*。系统中的每个主要组件都是通过抽象接口使用的,无论是图像,网格,粒子系统,渲染器,甚至是小部件和布局等UI概念都是抽象使用的。甚至我们的图像加载器/保护程序也是抽象的,因此只需在运行时添加插件(甚至是第三方编写的插件),软件就可以加载和保存以前无法识别的图像格式。 / p>

*在我们的例子中,我们的抽象接口使用C语言来使用函数指针表以获得最广泛的兼容性,但是在最常用的接口之上使用静态链接的C ++包装器使它们更安全,更容易使用。

模拟测试应该自然适合

在这种情况下,模拟测试框架非常适合。在这些情况下,你不必为了依赖注入而设计东西,这很自然。由于抽象接口构成了无法访问具体细节的软件的基础,因此别无选择,只能依赖于传入的其他抽象。

  

另外,您是否添加了定义公共接口的抽象基类?   到每个班级?如果你不这样做,你怎么能确定这个课程   永远不需要被嘲笑?

根据上述内容,您不应该仅仅为了依赖注入和模拟而使表依赖于抽象接口表面上。否则你可能会发现自己质疑这样的每一个小设计决定,这可能变成一种气味。应该有其他需求迫使您使这些中心的,广泛使用的接口抽象独立于模拟测试。应该有一些特性促使您在软件的核心寻求抽象,前提是它符合可扩展性/可扩展性要求,使模拟测试成为一种有用的策略。

并非每个项目都受益于大多数抽象界面

对于一些规模较小或非常严格定义的项目,寻求将所有中心接口抽象化将导致完全过度杀伤并最终产生适得其反的效果。在这种情况下,对严格定义的单元测试程序的需求并不强烈。在这种情况下,测试可能是单元测试和集成测试之间的模糊,并且在这种严格定义的,不可扩展的范围内完全可以接受。在这些类型的抽象案例中进行单元测试在团队环境中最有用,在这种环境中,您希望独立于Joe的工作来测试您的工作,这可能是错误的,或者将来可能会变得不正确。如果你是工作的唯一作者和维护者,并且控制着一切,通常是未知的最大来源之一被插入,世界不再在你的脚下移动,并且集成测试通常开始变得越来越有用,而单元测试的有用性,特别是涉及嘲弄的情况,似乎会减少。

集成测试

即使在所有依赖于抽象的代码库中,集成测试也非常有用。有时不幸的边缘情况只会在两个或多个具体实现组合在一起时出现,其中两个都在单独测试时通过测试但在组合时失败。

通常情况下会显示某些中间代码,这些中间代码都使用了一些模糊的时间耦合形式,就像这两个实体都可能使用某些图形库但图形库基于组合在一起时,您控制下的代码所要求的操作顺序。

然而,集成测试通常是大型项目的痛苦,因为它们通常需要从现实世界的输入构建复杂的结构。在这些情况下,我在C和C ++中发现的一个有用技巧是从插件中实际运行测试,为dylib提供他们可以提供的可选入口点函数,该函数仅用于测试目的。

这样主要的测试应用程序仍然可以构建" world" (在我们的例子中,一个场景图)用于测试,然后加载并执行适当的测试插件。这使得每个集成测试中通常需要的所有代码都可以启动系统,提前构建/加载所有必需的数据,关闭它等等,每个测试插件不再需要。我们只需在中央二进制文件中设置一次世界,然后加载相应的测试插件。它也倾向于鼓励不那么脆弱的测试,即使在集成测试领域,测试仍然在测试相当孤立的部分。仅仅通过人性,似乎当任何类型的测试需要大量的样板时,人们想要编写单片测试(不幸的是,这些测试往往更加脆弱)。

并非一切都应该是抽象的

即使您的项目符合模拟测试的必要抽象要求,通常也存在边缘情况。例如,即使在我的大部分系统依赖于通过SDK提供的抽象接口的情况下,我们也只有一小部分接口不是抽象的。

一个突出的例子是我们的数学库,它主要由线性代数的矢量/矩阵类模板组成。在这些情况下,数学库形成了一个稳定的根包(零传入耦合,正如罗伯特C.马丁将通过他的不稳定性度量来描述它):它并不依赖于其他任何东西。因此,这些库很容易单独进行单元测试。我们编写测试以确保矢量点积产生预期结果(预期结果在其他地方获得,经验证是正确的),例如。

如此稳定"根源"这类已经独立于世界的东西很容易被孤立地测试,即使不涉及任何抽象。有时,C ++模板在这里作为一种解耦机制可用于将类模板或函数模板与外部世界分离,使其完全独立。再一次,你不应该强迫所有东西成为一个类模板或功能模板,仅用于测试目的。除了可测试性之外,还有一个通用的,符合标准的序列容器比它更适合它。虽然可测试性绝对是一个强大的优势,但它并不是制作通用产品的最有力理由。

不要强行

无论如何,所以我的基本建议是不要强迫它。不要仅为了测试而将所有内容强制为独立的类/功能模板或抽象接口。第一个也是最重要的好处是动态或静态多态。首先应该存在可扩展性和可重用性问题,然后测试的简易性遵循仅依赖于抽象接口的代码的解耦性质。然而,仅仅为了可测试性,将整个项目中的所有依赖项表面重定向到抽象接口并不一定有效。尝试找到其他原因,使事情抽象而不仅仅考虑测试(尽管这是一个有用的目标)。