c++ - 牢记单元测试,重新构建C++应用程序

标签 c++ unit-testing architecture

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

我的问题是,您似乎需要为应用程序增加相当大的复杂度,以便建立使用模拟类所需的依赖项注入(inject)。

例如,您需要为所有可能需要模拟的类添加抽象基类,以便可以在生产代码中实例化“生产”对象,并在单元测试代码中实例化“模拟”对象。由于多余的类的数量以及对所有类的抽象级别的增加,这在某种程度上是不可取的。另外,您是否向每个类添加定义公共(public)接口(interface)的抽象基类?如果不这样做,您怎么能确保永远不需要 mock 该类呢?

或者,您需要对所有类进行模板化,以便可以在单元测试代码中“注入(inject)”模拟对象。我绝对不希望每个类都是模板类的应用程序。

每个人的经验如何?您如何将可测试性构建到体系结构中,结果如何?

最佳答案

For example, you would need to add abstract base classes for all classes that could need to be mocked so that you can instantiate "production" objects in the production code and "mock" objects in the unit test code. This is somewhat undesirable because of the number of extra classes and the added level of abstraction to all classes.



我认为,在基于模拟的测试框架变得很有道理之前,您首先必须意识到对此的需求。

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

抽象中央接口(interface)

所有这些代码库的共同点之一是其基础的软件开发套件。第三方会使用我们的SDK编写甚至有时为我们的产品出售插件,他们使用与构建主要产品相同的中央API来构建这些插件。

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

因此,在我们的案例中,非常需要软件的核心由抽象接口(interface)组成,而无论是否考虑进行单元测试*。系统中的每个主要组件都是通过抽象界面使用的,无论它是图像,网格,粒子系统,渲染器,甚至还抽象地使用了UI概念(如小部件和布局)。甚至我们的图像加载器/保存器也是抽象的,因此该软件只需在运行时添加一个插件(甚至是由第三方编写的插件)就可以加载和保存以前无法识别的图像格式。

*在我们的案例中,我们的抽象接口(interface)类似于C,使用函数指针表以实现最大的兼容性,但在最常用的接口(interface)之上使用静态链接的C++包装器,以使其更安全,更易于使用。

模拟测试应自然而然

在这种情况下,模拟测试框架自然适用。在这种情况下,您不必费力地设计用于依赖注入(inject)的事物,这很自然。由于抽象接口(interface)构成了无法访问具体细节的软件基础,​​因此别无选择,只能依靠传入的其他抽象。

Also, do you add an abstract base class defining the public interface to EVERY class? If you don't, how could you be sure that the class will never need to be mocked?



符合以上所述,您不必仅出于依赖关系注入(inject)和模拟的目的而使类表面上依赖抽象接口(interface)。否则,您可能会发现自己对这样的每个小小的设计决策都提出了质疑,而这可能会变成一种气味。还应该有其他需求迫使您将那些广泛使用的中央接口(interface)抽象为独立于模拟测试的抽象。应该有一些特征促使您在软件的核心中寻求抽象,前提是它符合使可伸缩性测试成为有用策略的那种可伸缩性/可扩展性要求。

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

对于一些较小的或非常严格定义的项目,试图使所有中央接口(interface)抽象化将是完全的矫kill过正,最终适得其反。在这种情况下,对严格定义的单元测试过程的需求就不那么强烈了。在这种情况下,测试可能是单元测试和集成测试之间的模糊,在这种严格定义的,不可扩展的范围内,这是完全可以接受的。这些类型的抽象案例中的单元测试在团队环境中最有用,在团队环境中,您想独立且独立于Joe的工作来测试您的工作,这可能是不正确的,或者将来可能变得不正确。如果您是这项工作的唯一作者和维护者,并且掌控一切,通常是最大的未知源之一被塞入了世界,您的脚将不再处于停滞不前的状态,而集成测试通常会开始变得越来越有用。单元测试的有用性,尤其是在涉及模拟的情况下,似乎会减少。

集成测试

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

通常,当存在某种中间代码,并且两者都以某种晦涩的形式出现时,它们就会显示出来,例如,这两个实体都可能使用某些图形库,但是图形库根据代码下的操作顺序进行了一些时髦的处理您的控制请求组合在一起时。

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

这样,主测试应用程序仍可以构造用于测试的“世界”(在我们的示例中为场景图),然后加载并执行适当的测试插件。这样一来,每个测试插件中就不再需要每个集成测试中通常所需的所有代码来启动系统,预先构造/加载所有必需的数据,将其关闭等。我们只需要在一个中央二进制文件中设置一次世界,然后加载适当的测试插件即可。这也只会鼓励不那么脆弱的测试,即使在集成测试领域中,这些测试仍在测试相当孤立的零件。仅凭人类本性,似乎当任何类型的测试都需要大量样板时,人们希望编写整体测试(不幸的是,这种测试往往更脆弱)。

并非所有内容都应该抽象

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

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

如此稳定的,已经独立于世界的“根”,即使不涉及任何抽象,也易于隔离测试。有时,C++模板在这里用作将类模板或函数模板与外界分离,使其完全独立的一种解耦机制。再次重申,您不必只出于测试目的就将所有内容强制成为类模板或功能模板。作为一个通用的,符合标准的序列容器,它具有比可测试性更多的功能。尽管可测试性绝对是一个强项,但这并不是使某些东西通用的最强理由。

不要强制

无论如何,所以我的基本建议是不要强制它。不要仅仅为了测试而将所有内容强制成为独立的类/函数模板或抽象接口(interface)。第一个也是最重要的好处是动态或静态多态性。首先,应该首先考虑到可扩展性和可重用性,然后,测试的简便性遵循的是代码的解耦性质,这完全取决于抽象接口(interface)。但是,仅出于可测试性,将整个项目中的所有依赖关系表面地重定向到抽象接口(interface)并不一定具有生产力。尝试寻找其他使事情变得抽象的原因,而不必仅仅进行测试(尽管这是一个有用的目标)。

关于c++ - 牢记单元测试,重新构建C++应用程序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34158399/

相关文章:

c++ - 在多线程环境中管理 sqlite 数据库

c++ - 二叉树 : iterative inorder print

java - 编写/实现 API : testability vs information hiding

unit-testing - 在 Jenkins 中构建子项目时单元测试结果未显示在 Sonar 中

Flutter 实现整洁架构的方式

c++ - 将大型单 block 单线程应用程序转换为多线程体系结构的建议?

c++ - 从 C++ 调用 Fortran 子程序

c++ - strtok_r函数如何返回值?

python - Python 开源项目的正常结构是什么?运行测试的首选方式是什么?

java - 了解服务和 DAO 层