如何将单元测试引入到一个大型的遗留(C/C + +)代码库中?

我们有一个用 C 语言编写的大型多平台应用程序(C + + 的数量虽然不多,但仍在不断增加)。这些年来,它已经发展成为一个大型 C/C + + 应用程序所具备的许多特性:

  • #ifdef见鬼
  • 大文件使得很难隔离可测试代码
  • 过于复杂而不易测试的函数

由于这段代码是针对嵌入式设备的,因此在实际目标上运行它会带来很大的开销。因此,我们希望在本地系统上进行更多快速周期的开发和测试。但是,我们希望避免使用“复制/粘贴到。系统中的 c 文件,修复 bug,复制/粘贴回来”。如果开发人员不辞辛劳地这样做,我们希望以后能够重新创建相同的测试,并以自动化的方式运行。

这就是我们的问题: 为了使代码更加模块化,我们需要它更具有可测试性。但是为了引入自动化的单元测试,我们需要它更加模块化。

一个问题是,由于我们的文件非常大,我们可能有一个函数在一个文件中调用函数 在同一个文件里,我们需要存根,使一个良好的单元测试。当我们的代码变得更加模块化时,这似乎不是什么问题,但是这还有很长的路要走。

我们想做的一件事就是用注释标记“已知可测试”的源代码。然后,我们可以编写一个脚本,扫描可测试代码的源文件,在一个单独的文件中编译它,并将它与单元测试链接起来。当我们修复缺陷并添加更多功能时,我们可以慢慢地引入单元测试。

然而,有人担心维护这个方案(以及所有必需的存根函数)会变得太麻烦,开发人员将停止维护单元测试。因此,另一种方法是使用一种工具,自动为所有代码生成存根,并将该文件与存根链接起来。(我们发现唯一可以做到这一点的工具是一个昂贵的商业产品)但是这种方法在 要求看来,在我们可以开始之前,我们所有的代码都是更模块化的,因为只有外部调用可以被删除。

就个人而言,我宁愿让开发人员考虑他们的外部依赖性,并明智地编写自己的存根。但是,这可能会导致无法消除对一个过度增长的10,000行文件的所有依赖性。可能很难让开发人员相信他们需要维护所有外部依赖项的存根,但这是正确的方法吗?(我听到的另一个观点是,子系统的维护者应该维护他们子系统的存根。但我想知道“强制”开发人员编写自己的存根是否会导致更好的单元测试?)

当然,#ifdefs为这个问题增加了一个完整的维度。

我们已经研究了几个基于 C/C + + 的单元测试框架,有很多选项看起来都不错。但是我们还没有找到任何东西来缓解从“没有单元测试的毛球代码”到“单元可测试代码”的转变。

所以我想问问其他经历过这些的人:

  • 什么是一个好的起点? 我们的方向是正确的,还是我们遗漏了一些显而易见的东西?
  • 什么工具可能有助于转型?(最好是免费/开源,因为我们现在的预算大约是“零”)

注意,我们的构建环境是基于 Linux/UNIX 的,因此我们不能使用任何仅适用于 Windows 的工具。

17523 次浏览

首先要使它更加模块化要容易得多。您不能真正地对具有很多依赖项的内容进行单元测试。何时进行重构是一个棘手的计算。你真的必须权衡成本和风险与收益。这些代码是否可以被广泛地重用?或者这个代码真的不会改变。如果您打算继续使用它,那么可能需要重构。

但是听起来,您需要重构。您需要从分解最简单的实用程序开始,并在它们的基础上进行构建。你的 C 模块可以做很多事情。例如,也许有些代码总是以某种方式格式化字符串。也许这可以被提出来作为一个独立的实用模块。你已经有了新的字符串格式化模块,你已经使代码更具可读性。这已经是进步了。你声称自己陷入了两难的境地。你真的不是。通过移动代码,可以使代码更具可读性和可维护性。

现在您可以为这个断开的模块创建一个单元测试。你可以有几种方法。你可以创建一个单独的应用程序,它只包含你的代码,并在你的电脑上的一个主例程中运行一系列的用例,或者定义一个名为“ UnitTest”的静态函数,它将执行所有的测试用例,如果它们通过,返回“1”。这个可以在目标上运行。

也许您不能100% 使用这种方法,但是这只是一个开始,它可能使您看到其他可以轻松分解为可测试实用程序的东西。

要考虑的一种方法是首先放置一个系统范围的模拟框架,您可以使用这个框架来开发集成测试。从集成测试开始似乎有违直觉,但是在您描述的环境中进行真正的单元测试的问题是相当可怕的。可能不仅仅是在软件中模拟整个运行时..。

这种方法将简单地绕过您列出的问题——尽管它会给您带来许多不同的问题。但是在实践中,我发现使用一个健壮的集成测试框架,您可以开发在单元级别执行功能的测试,尽管不需要单元隔离。

PS: 考虑编写一个命令驱动的模拟框架,可以在 Python 或 Tcl 上构建。这将使您可以非常容易地编写测试脚本..。

你好,

我会先看看任何明显的地方,例如在头文件中使用 dec。

然后开始查看代码是如何布局的。这符合逻辑吗?也许开始把大文件分解成小文件。

也许可以借用 Jon Lakos 的优秀著作《大规模 C + + 软件设计》(Large-Scale C + + Software Design,经过消毒的亚马逊链接) ,从中得到一些关于如何布局的想法。

一旦你开始对代码本身更有信心,比如代码布局和文件布局一样,并且清除了一些不好的味道,比如在头文件中使用 dec,那么你就可以开始挑选一些可以用来开始编写单元测试的功能。

选择一个好的平台,我喜欢 CUnit 和 CPPUnit,并从那里开始。

不过这将是一段漫长而缓慢的旅程。

高温

干杯,

我们还没有找到任何东西来缓解“毛球”的过渡 没有单元测试的代码”到“可单元测试的代码”。

多么悲哀——没有奇迹般的解决方案——只是许多艰苦的工作纠正了多年来积累的 技术债务

没有简单的过渡,你有一个大的,复杂的,严重的问题。

你只能用很小的步骤来解决它。

  1. 选择一段绝对重要的离散代码。(不要轻咬垃圾食品的边缘。)选择一个重要的部件,然后——不知怎么地——可以从其他部件中雕刻出来。虽然单个函数是理想的,但它可能是一个复杂的函数集群,也可能是一个完整的函数文件。对于您的可测试组件来说,从不完美的事物开始是可以的。

  2. 弄清楚它的作用。弄清楚它的接口应该是什么。要做到这一点,您可能需要进行一些初始重构,以使您的目标部分实际上是离散的。

  3. 编写一个“整体”集成测试,该测试目前或多或少地测试您的离散代码片段。在你尝试改变任何重要的事情之前,让这个过去。

  4. 将代码重构成整洁的、可测试的单元,使其比当前的毛球更有意义。你需要在整体集成测试中保持一些向下兼容。

  5. 为新单元编写单元测试。

  6. 一旦这一切都通过了,废除旧的 API 并修复因更改而破坏的内容。如果有必要,重新进行原始的集成测试; 它测试旧的 API,您需要测试新的 API。

重复。

迈克尔 · 费瑟斯在这上面写了圣经 有效使用遗留代码

我在遗留代码和引入测试方面的一点经验就是创建“ 角色塑造测试”。您开始创建具有已知输入的测试,然后获得输出。这些测试对于那些您不知道它们实际上是做什么的方法/类很有用,但是您知道它们是有效的。

然而,有时几乎不可能创建单元测试(甚至是角色塑造测试)。在这种情况下,我通过验收测试(本例中为 健身)来解决问题。

您创建了测试一个特性并检查其适应性所需的所有类。它类似于“角色塑造测试”,但它更高一级。

我在 Green field 项目中工作过,使用了完整的单元测试代码库和大型 C + + 应用程序,这些应用程序已经发展了很多年,并且有很多不同的开发人员在使用它们。

老实说,我不会费心尝试将遗留代码基础设置为单元测试和测试优先开发可以增加很多价值的状态。

一旦遗留代码基达到了一定的规模和复杂度,单元测试覆盖率为您提供了很多好处,这个任务就相当于完全重写。

主要问题是,一旦开始重构可测试性,就会开始引入 bug。只有当您获得高的测试覆盖率时,您才能期望发现并修复所有这些新的 bug。

这意味着您要么非常缓慢而小心地前进,要么直到多年以后才能从良好的单元测试代码库中获得好处。(可能自从合并等事情发生后就再也没有了。)与此同时,您可能正在引入一些对软件的最终用户没有明显价值的新 bug。

或者您进行得很快,但是代码基础不稳定,直到所有代码都达到了较高的测试覆盖率。(所以最终会有两个分支,一个在生产环境中,一个在单元测试版本中。)

当然,对于一些项目来说,重写可能只需要几个星期的时间,而且肯定是值得的。

正如乔治所说,有效地使用遗留代码就是这类事情的圣经。

然而,您团队中的其他人能够接受的唯一方法是,他们能够亲眼看到保持测试工作对他们的好处。

要实现这一点,您需要一个尽可能易于使用的测试框架。为其他开发人员制定计划,以您的测试为例编写他们自己的测试。如果他们没有单元测试经验,不要指望他们花时间学习一个框架,他们可能会认为编写单元测试会减慢他们的开发,所以不知道框架是跳过测试的借口。

花一些时间在使用巡航控制,半自动驾驶,快速行驶等持续集成上。如果您的代码每晚都自动编译并运行测试,那么开发人员将开始看到单元测试在 qa 之前捕获 bug 的好处。

鼓励共享代码所有权。如果一个开发人员更改了他们的代码并破坏了其他人的测试,他们不应该期望那个人修复他们的测试,他们应该调查为什么测试不能工作并自己修复它。根据我的经验,这是最难做到的事情之一。

大多数开发人员编写某种形式的单元测试,有时是一小段他们没有签入或集成构建的一次性代码。轻松地将这些集成到构建中,开发人员就会开始买账。

我的方法是添加针对新代码的测试,当代码被修改时,有时您无法添加您想要的那么多或者那么详细的测试,而不需要解耦太多的现有代码,因此在实际应用中会出现错误。

我唯一坚持单元测试的地方是在平台特定的代码上。如果将 # ifdefs 替换为平台特定的高级函数/类,则必须使用相同的测试在所有平台上测试这些函数/类。这节省了添加新平台的大量时间。

我们使用 ost: : test 来构建我们的测试,简单的自注册函数使编写测试变得容易。

这些都包装在 CTest (CMake 的一部分)中,它一次运行一组单元测试可执行文件并生成一个简单的报告。

我们的夜间构建通过 ant 和 luntbuild (ant glues c + + ,. net 和 java 构建)实现自动化

很快,我希望向构建中添加自动部署和功能测试。

我认为,基本上你有两个独立的问题:

  1. 用于重构的大代码库
  2. 团队合作

模块化、重构、插入单元测试等等都是一项艰巨的任务,我怀疑任何工具都无法承担这项工作的更大部分。这是一种罕见的技能。有些程序员可以做得很好。大多数人都不喜欢。

与一个团队一起完成这样的任务是乏味的。我强烈怀疑“强迫”开发人员的做法是否会奏效。Iains 的想法很好,但是我会考虑找到一两个能够并且想要“清理”源代码的程序员: 重构,Modualrize,引入单元测试等等。让这些人做这项工作,其他人引入新的错误,嗯的功能。只有从事 喜欢那种工作的人才能胜任那项工作。

使使用测试变得容易

我会从“自动运行”开始。如果您希望开发人员(包括您自己)编写测试,那么应该使运行它们变得容易,并查看结果。

编写一个包含三行代码的测试,在最新版本中运行它并查看结果应该是 只需要点击一下,而不是将开发人员发送到咖啡机。

这意味着你需要一个最新的版本,你可能需要改变策略,人们如何工作的代码等。我知道这样的过程可以是一个带有嵌入式设备的 PITA,对此我不能给出任何建议。但是我知道如果运行测试很困难,没有人会编写它们。

测试可以测试的内容

我知道我在这里违背了常见的单元测试哲学,但这就是我所做的: 为容易测试的东西编写测试。我不会为嘲笑而烦恼,我不会为了让它可测试而重构,而且如果涉及到 UI,我也不会有单元测试。但我在图书馆的日常生活中越来越多地出现了这种情况。

我非常惊讶于简单的测试会发现什么。摘取低垂的果实决不是无用的。

从另一个角度来看: 如果它不是一个成功的产品,你就不会计划维持这个巨大的毛球状的混乱。您目前的质量控制不是一个需要更换的完全失败。相反,在容易做的地方使用单元测试。

(但是,您需要完成它,不要陷入围绕您的构建过程“修复所有东西”的困境。)

教授如何改进代码库

任何具有这种历史记录的代码库都需要改进,这是肯定的。

看到两段具有相同功能的代码,大多数人都会同意在给定的方面(性能、可读性、可维护性、可测试性,... ...)下哪一段代码“更好”。最难的部分有三个:

  • 如何平衡不同方面
  • 如何同意这段代码是 很好
  • 如何在不破坏任何东西的情况下将糟糕的代码转化为足够好的代码。

第一个问题可能是最难的,它既是一个工程问题,也是一个社会问题。但是其他方面是可以学习的。我不知道任何正式的课程采取这种方法,但也许你可以组织内部的东西: 从两个家伙一起工作到“研讨会”,在那里你采取讨厌的代码片段,并讨论如何改进它。


我们正在这么做。三年前,我加入了一个项目开发团队,这个项目没有单元测试,几乎没有代码审查,而且是一个相当特别的构建过程。

代码库由一组 COM 组件(ATL/MFC)、跨平台 C + + Oracle 数据卡和一些 Java 组件组成,所有这些组件都使用跨平台 C + + 核心库。有些代码已经有将近十年的历史了。

第一步是添加一些单元测试。不幸的是,这种行为是非常数据驱动的,因此在生成单元测试框架(最初是 CppUnit,现在通过 JUnit 和 NUnit 扩展到其他模块)方面有一些初始工作,它使用来自数据库的测试数据。大多数最初的测试都是功能性测试,它们处理了最外层,而不是真正的单元测试。您可能不得不花费一些精力(您可能需要预算)来实现测试装具。

我发现,如果您将添加单元测试的成本尽可能地降低,那么它将大有帮助。测试框架使得在修复现有功能中的错误时添加测试相对容易,新代码可以拥有适当的单元测试。当您重构和实现新的代码区域时,您可以添加适当的单元测试来测试更小的代码区域。

去年,我们增加了与 CruiseControl 的持续集成,并实现了构建过程的自动化。这为保持测试更新和通过提供了更多的动力,这在早期是一个大问题。因此,我建议您将定期(至少每晚)单元测试运行作为开发过程的一部分。

我们最近关注于改进我们的代码审查过程,这是相当罕见和无效的。这样做的目的是降低启动和执行代码审查的成本,从而鼓励开发人员更频繁地进行代码审查。另外,作为我们过程改进的一部分,我试图为项目计划中包含的代码审查和单元测试争取时间,这样可以确保个人开发人员更多地考虑它们,而以前只有固定比例的时间投入到这些工作中,很容易在计划中迷失。

这一切都有一个哲学层面。

您真的想要经过测试、功能齐全、整洁的代码吗?这是你的目标吗?你从中得到了什么好处吗?.

是的,一开始这听起来很愚蠢。但老实说,除非你是系统的实际拥有者,而不仅仅是一个员工,那么 bug 只意味着更多的工作,更多的工作意味着更多的钱。当你处理毛球的时候,你会非常开心。

我只是在这里猜测,但是,你所冒的风险,采取这个巨大的斗争可能是高得多的回报,你可能得到的代码整洁。如果你缺乏社交技巧来解决这个问题,你只会被视为一个麻烦制造者。我见过这些人,我也是其中之一。不过当然,如果你能挺过去,那就太酷了。我会印象深刻的。

但是,如果您觉得自己被迫现在花费额外的时间来保持一个不整洁的系统正常工作,那么您真的认为一旦代码变得整洁和漂亮,这种情况就会改变吗?.没有。.一旦代码变得漂亮整洁,人们就会有更多的空闲时间在第一个可用的截止日期再次完全销毁它。

归根结底,是管理创造了美好的工作环境,而不是代码。

不知道是不是真的,但我有个小建议。据我所知,您提出的方法论问题是关于将单元测试增量非侵入性地集成到巨大的遗留代码中,并由许多涉众保护他们的沼泽。

通常,第一步是独立于所有其他代码构建测试代码。即使是长期遗留代码中的这一步也是非常复杂的。我建议将您的测试代码构建为具有运行时链接的动态共享库。这将允许您只重构测试不足的一小段代码,而不是整个20K 文件。因此,您可以开始逐个函数覆盖函数,而无需触及/修复所有链接问题