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

mpo*_*llo 73 c c++ unix legacy unit-testing

我们有一个用C语言编写的大型多平台应用程序(只有少量但不断增长的C++)多年来,它已经发展了许多你期望在大型C/C++应用程序中使用的功能:

  • #ifdef 地狱
  • 大文件使得难以隔离可测试代码
  • 功能过于复杂,无法轻松测试

由于此代码是针对嵌入式设备的,因此在实际目标上运行它需要大量开销.因此,我们希望在本地系统上以快速周期进行更多的开发和测试.但我们希望避免"在您的系统上复制/粘贴到.c文件,修复错误,复制/粘贴"的经典策略.如果开发人员要麻烦这样做,我们希望以后能够重新创建相同的测试,并以自动方式运行.

这是我们的问题:为了使代码重构更加模块化,我们需要它更易于测试.但是为了引入自动化单元测试,我们需要它更加模块化.

一个问题是,由于我们的文件太大,我们可能在文件中有一个函数调用一个文件中的函数,我们需要将它们存根以进行良好的单元测试.看起来这不是一个问题,因为我们的代码变得更加模块化,但这还有很长的路要走.

我们想到的一件事是用注释标记"已知可测试"的源代码.然后我们可以为可测试代码编写脚本扫描源文件,将其编译在单独的文件中,并将其与单元测试链接.我们可以在修复缺陷和添加更多功能时慢慢引入单元测试.

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

就个人而言,我宁愿让开发人员考虑他们的外部依赖关系并智能地编写他们自己的存根.但是,对于一个可怕的过度生长的10000行文件来说,这可能是压倒性的.可能很难说服开发人员他们需要为所有外部依赖项维护存根,但这是正确的方法吗?(我听到的另一个论点是子系统的维护者应该维护子系统的存根.但是我想知道"强迫"开发人员编写自己的存根会导致更好的单元测试吗?)

#ifdefs,当然,再添全尺寸的问题.

我们已经研究了几个基于C/C++的单元测试框架,并且有很多选项看起来很好.但是我们还没有找到任何方法来缓解从"没有单元测试的代码毛球"到"单元可测试代码"的过渡.

所以这是我对其他任何经历过这个问题的人的问题:

  • 什么是一个好的起点?我们是朝着正确的方向前进,还是我们错过了一些明显的东西?
  • 哪些工具可能有助于过渡?(最好是免费/开源,因为我们现在的预算大致为"零")

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

S.L*_*ott 49

我们还没有找到任何方法来缓解从"没有单元测试的代码毛球"到"单元可测试代码"的过渡.

多么悲伤 - 没有奇迹般的解决方案 - 只需要很多艰苦的工作来纠正多年积累的技术债务.

没有简单的过渡.你有一个庞大而复杂的严重问题.

你只能用很小的步骤解决它.每个小步骤涉及以下内容.

  1. 选择一段绝对必要的独立代码.(不要在垃圾边缘蚕食.)选择一个重要的组件,并且 - 不知何故 - 可以从其余部分中雕刻出来.虽然单个函数是理想的,但它可能是一个纠结的函数集群,也可能是整个函数文件.可以从可测试组件的不完美开始.

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

  3. 编写一个"整体"集成测试 - 现在 - 测试你的离散代码片段或多或少.在尝试更改任何重要内容之前,请先通过此操作.

  4. 将代码重构为整洁,可测试的单元,使其比您当前的毛球更有意义.您将不得不在整体集成测试中保持一些向后兼容性(目前).

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

  6. 一旦通过,退出旧API并修复将被更改破坏的内容.如有必要,重新进行原始集成测试; 它测试旧的API,你想测试新的API.

重复.

  • 如果您不从核心功能开始,则测试是可选的.经理将决定不需要进行测试并放弃测试.如果从核心开始,测试就变得至关重要. (4认同)
  • @Mike:很简单.管理冲突 - 不可避免地 - 将会破坏努力,除非你深入研究重要的事情.情况很糟糕,但并不复杂.当模块的测试被取消或否决时,保存您的电子邮件并明知. (3认同)

Geo*_*lly 25

迈克尔·弗拉斯(Michael Feathers)就这一点撰写了圣经" 与遗产守则有效合作"

  • 本书的简短版本:http://www.objectmentor.com/resources/articles/WorkingEffectivelyWithLegacyCode.pdf (6认同)

Edi*_*enz 8

我对遗留代码和引入测试的小经验将是创建" 特征化测试 ".您开始使用已知输入创建测试,然后获取输出.这些测试对于你不知道它们真正做什么的方法/类很有用,但是你知道它们正在工作.

但是,有时候几乎不可能创建单元测试(甚至表征测试).在那种情况下,我通过验收测试(在这种情况下为Fitnesse)来解决问题.

您可以创建测试一个功能所需的一大堆类,并在fitnesse上进行检查.它与"特征测试"类似,但它高一级.


iai*_*ain 7

正如乔治所说,有效地使用遗产代码是这种事情的圣经.

然而,团队中其他人购买的唯一方法是,如果他们看到他们个人对保持测试有效的好处.

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

花一些时间使用巡航控制,luntbuild,cdash等进行持续集成.如果你的代码每晚自动编译并运行测试,那么开发人员将开始看到如果单元测试在qa之前捕获错误的好处.

鼓励的一件事是共享代码所有权.如果开发人员更改了他们的代码并破坏了其他人的测试,他们不应该期望该人修复他们的测试,他们应该调查测试不起作用的原因并自行修复.根据我的经验,这是最难实现的事情之一.

大多数开发人员都会编写某种形式的单元测试,有时候是他们不会检查或整合构建的一小段丢弃代码.将这些集成到构建中很容易,开发人员将开始购买.

我的方法是为new添加测试,并且在修改代码时,有时你不能添加尽可能多的或详细的测试而不需要分离太多的现有代码,错误的是实际的.

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

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

它们包含在CTest(CMake的一部分)中,它同时运行一组单元测试可执行文件并生成一个简单的报告.

我们的夜间构建是使用ant和luntbuild自动构建的(ant glues c ++,.net和java build)

很快我希望为构建添加自动部署和功能测试.


小智 5

我们正在做这件事.三年前,我加入了开发团队的项目,没有进行单元测试,几乎没有代码审查,还有一个相当特殊的构建过程.

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

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

如果你把单位测试的成本降到最低,我发现它会有很大的帮助.测试框架使得在修复现有功能中的错误时添加测试相对容易,新代码可以进行适当的单元测试.在重构和实现新的代码区域时,您可以添加适当的单元测试,以测试更小的代码区域.

在过去的一年中,我们增加了与CruiseControl的持续集成并自动化我们的构建过程.这增加了更多的动力来保持测试的最新和通过,这在早期是一个大问题.因此,我建议您在开发过程中包含常规(至少每晚)单元测试运行.

我们最近专注于改进我们的代码审查流程,这种流程非常罕见且无效.目的是使启动和执行代码审查的成本更低,以便鼓励开发人员更频繁地执行它们.此外,作为我们流程改进的一部分,我正在努力将项目规划中包含的代码审查和单元测试花费在更低的水平上,以确保个别开发人员必须更多地考虑它们,而以前只有一个固定的比例专注于他们的时间更容易迷失在日程安排中.


Dou*_* T. 2

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

听起来你想重构。您需要从分解最简单的实用程序开始,并在它们的基础上进行构建。你的 C 模块可以完成无数的事情。例如,也许其中有一些代码总是以某种方式格式化字符串。也许这可以成为一个独立的实用模块。您已经有了新的字符串格式化模块,使代码更具可读性。它已经是一个进步了。您声称自己处于第 22 条军规的情况。你真的不是。只需移动一些东西,就可以使代码更具可读性和可维护性。

现在您可以为这个分解的模块创建一个单元测试。您可以通过几种方式做到这一点。您可以制作一个单独的应用程序,其中仅包含您的代码并在 PC 上的主例程中运行一堆案例,或者定义一个名为“UnitTest”的静态函数,该函数将执行所有测试用例并在通过时返回“1”。这可以在目标上运行。

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


归档时间:

查看次数:

14918 次

最近记录:

7 年,9 月 前