如何组织C ++单元测试代码以实现最大的单元测试效率?


47
  • 这个问题与单元测试框架无关
  • 这个问题与编写单元测试无关
  • 这个问题是关于在哪里放置UT代码以及如何/何时/在哪里编译和运行它。

有效地使用旧版代码中,迈克尔·费瑟斯断言

良好的单元测试...运行速度快

然后

运行1/10秒的单元测试是缓慢的单元测试。

我认为这些定义确实有意义。我还认为,这意味着您必须分别保留一组单元测试和一组代码测试这些测试需要更长的时间,但是我想这就是您仅在运行(非常快)时才调用单元测试的价格。

显然,问题在C ++中是“跑”单元测试(小号),你必须:

  1. 编辑代码(生产或单元测试,具体取决于您所在的“周期”)
  2. 编译
  3. 链接
  4. 启动单元测试可执行文件(小号

编辑(经过奇怪的近距离表决):在进入细节之前,我将尝试在此处总结要点:

如何有效地组织C ++单元测试代码,这样既可以有效地编辑(测试)代码又可以运行测试代码?


然后,第一个问题是确定将单元测试代码放在哪里,以便:

  • 结合相关的生产代码进行编辑和查看是“自然的”。
  • 轻松/快速地为您当前更改的单元开始编译周期

第二个的话,相关的,问题是什么来编译,这样的反馈是瞬时的。

极端的选择:

  • 每个单元测试-测试单元都位于一个单独的cpp文件中,并且此cpp文件被单独编译+链接(连同它测试的源代码单元文件)到单个可执行文件,然后该可执行文件运行此一个单元测试。
    • (+)这样可以最小化单个测试单元的启动(编译+链接!)时间。
    • (+)测试运行非常快,因为它仅测试一个单元。
    • (-)执行整个套件将需要启动大量流程。可能是一个管理难题。
    • (-)流程启动的开销将变得可见
  • 另一面将是-仍然-每个测试一个cpp文件,但是所有测试cpp文件(以及它们测试的代码!)都链接到一个可执行文件中(每个模块/每个项目/选择您的选择)。
    • (+)编译时间仍然可以,因为只有更改的代码才能编译。
    • (+)执行整个套件很容易,因为只有一个exe可以运行。
    • (-)套件将花费很多时间进行链接,因为任何对象的每次重新编译都会触发重新链接。
    • (-)(?)套装将花费更长的时间运行,尽管如果所有单元测试都很快,则时间应该可以。

那么,现实世界中的C ++ 单元测试如何处理?如果我只是每晚/每小时运行一次,那么第二部分并不重要,但是第一部分,即如何将UT代码“耦合”到生产代码,这样对于开发人员来说,将两者保持一致是“自然的”我认为专注始终很重要。(如果开发人员将UT代码作为重点,他们将要运行它,这将使我们回到第二部分。)

欣赏真实世界的故事和经验!

笔记:

  • 该问题有意离开了未指定的平台和品牌/项目系统。
  • 问题带有标记的UT&C ++是一个很好的起点,但是不幸的是,太多的问题(尤其是答案)过于注重细节或特定框架。
  • 前一阵子,我回答了关于升压单元测试的结构的类似问题。我发现这种结构对于“真实的”快速单元测试是缺少的。我发现另一个问题过于狭窄,因此提出了这个新问题。

6
C ++惯用语引入了进一步的复杂性,即移动尽可能多的错误来编译时间-一组好的单元测试通常需要能够测试某些用法无法编译。

3
@Closers:能否请您提供引起您解决此问题的论点?
卢克·图拉耶

我不太明白为什么必须要关闭它。:-(如果不在本论坛中,您应该在哪里寻找此类问题的答案?

2
@Joe:为什么当编译器告诉我这一切时,为什么需要进行单元测试以查找是否会编译某些东西?
David Thornley

2
@David:因为您要确保它不会编译。一个简单的例子是一个接受输入并产生输出的管道对象,Pipeline<A,B>.connect(Pipeline<B,C>)应该编译而不Pipeline<A,B>.connect(Pipeline<C,D>)应该编译:第一级的输出类型与第二级的输入类型不兼容。
sebastiangeiger

Answers:


6

我们在一个可执行文件中包含了所有单元测试(针对一个模块)。将测试分组。通过在测试运行程序的命令行中指定(测试/组)名称,我可以执行单个测试(或某些测试)或一组测试。构建系统可以运行组“ Build”,测试部门可以运行“ All”。开发人员可以将一些测试放入“ BUG1234”之类的组中,其中1234是他正在处理的案例的问题跟踪号。


6

首先,我不同意“ 1)编辑您的(生产)代码和单元测试”。您一次只能修改一个,否则,如果结果更改,您将不知道是哪个原因引起的。

我喜欢将单元测试放在隐藏主树的目录树中。如果我有/sources/componentA/alpha/foo.ccand /objects/componentA/beta/foo.o,那么我想要类似/UTest_sources/componentA/alpha/test_foo.ccand的东西/UTest_objects/componentA/beta/test_foo.o。对于存根/模拟对象以及测试所需的其他任何来源,我都使用相同的影子树。会有一些极端情况,但是这种方案大大简化了工作。一个好的编辑器宏可以毫不费力地将测试源与主题源一起拉起。一个好的构建系统(例如GNUMake)可以编译并使用一个命令(例如make test_foo)运行测试,并且可以管理如此庞大的进程-只是自上次测试以来其源已更改的进程-非常容易(我有从来没有发现启动这些过程的开销是个问题,这是O(N))。

在同一个框架中,您可以具有将许多对象链接在一起并运行许多测试的大规模测试(不再是单元测试)。诀窍是按照构建/运行所需的时间对这些测试进行排序,并将其相应地纳入您的日常计划中。随时进行一秒钟或更短的测​​试;开始十秒钟的测试并拉伸;五分钟的测试,休息一下;半小时的考试,去吃午餐;六个小时的测试然后回家。如果您发现浪费大量时间,例如仅更改一个小文件后重新链接一个巨大的测试,那您就做错了-即使链接是瞬时的,当它仍然运行时,您仍然会运行一个长时间的测试没有被要求。


广告(1)-是的,措辞很含糊
Martin Ba
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.