你如何将unit testing引入一个大的,传统的(C / C ++)代码库?

我们有一个用C语言编写的大型多平台应用程序(只有less量的C ++)。多年来,随着C / C ++大型应用程序中许多function的不断发展,

  • #ifdef地狱
  • 大的文件,很难隔离可testing的代码
  • function太复杂,不易testing

由于这个代码是针对embedded式设备的,所以在实际的目标上运行它会花费很多的开销。 因此,我们希望在本地系统上进行更多快速周期的开发和testing。 但是我们希望避免“复制/粘贴到您的系统上的.c文件,修复错误,复制/粘贴”的经典策略。 如果开发人员要麻烦去做,我们希望能够在以后重新创build相同的testing,并以自动化的方式运行。

这是我们的问题:为了重构代码,使其更加模块化,我们需要它更加可testing。 但是为了引入自动化的unit testing,我们需要它更加模块化。

一个问题是,由于我们的文件太大了,我们可能会在一个文件里面有一个函数,在同一个文件中调用一个函数,我们需要把这个文件作为一个unit testing。 看起来这样做会less一些问题,因为我们的代码变得更加模块化了,但是还有很长的一段路要走。

我们想要做的一件事就是用注释标记“已知是可testing的”源代码。 然后,我们可以为可testing代码编写一个脚本扫描源文件,将其编译为单独的文件,并将其与unit testing链接。 我们可以在修复缺陷并添加更多function时,慢慢地引入unit testing。

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

就个人而言,我宁愿让开发人员考虑他们的外部依赖关系,聪明地写出他们自己的存根。 但是,这可能是压倒性的,以排除一个可怕的杂草丛生的10,000行文件的所有依赖关系。 要说服开发人员需要为所有的外部依赖维护存根可能是困难的,但这是否正确? (另一个我听说的论点是,一个子系统的维护者应该为他们的子系统维护存根,但是我想知道是否迫使开发者写他们自己的存根会导致更好的unit testing?)

#ifdefs当然会为这个问题增加另外一个维度。

我们已经看过几个基于C / C ++的unit testing框架,并且有很多选项看起来不错。 但是我们还没有find任何东西来缓解从“没有unit testing代码的毛球”到“unit testing代码”的过渡。

所以这里是我的问题给任何其他谁已经通过这个问题:

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

请注意,我们的构build环境是基于Linux / UNIX的,所以我们不能使用任何仅限于Windows的工具。

“我们还没有find任何东西来缓解从”没有unit testing代码的毛球“到”unit testing代码“的过渡。

多么悲伤 – 没有奇迹般的解决scheme – 只是很多辛苦的工作,纠正多年积累的技术债务 。

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

你只能以微小的步骤解决它。 每一个微小的步骤涉及以下。

  1. select一个绝对必要的代码片段。 (不要在垃圾边缘啃边。)select一个非常重要的组件,不知何故 – 可以从其他组件中切割出来。 虽然单一的function是理想的,但它可能是一个纠结的function集群,或者可能是一个完整的function文件。 可以从可testing组件开始,使用不完美的东西。

  2. 找出它应该做什么。 找出它的接口应该是什么。 要做到这一点,你可能不得不做一些初始的重构,使你的目标件实际上离散。

  3. 写一个“整体”集成testing,现在 – testing你的离散的代码或多或less的发现。 在尝试和改变任何重要的东西之前,先把它传递出去。

  4. 将代码重构为整洁,可testing的单元,比当前的毛球更有意义。 你将不得不保持一些向后兼容性(现在)与整体集成testing。

  5. 为新单位编写unit testing。

  6. 一旦全部通过,就停用旧的API并修复由于更改而被破坏的内容。 如有必要,重新进行原始集成testing; 它testing旧的API,你想testing新的API。

迭代。

迈克尔羽毛写了这个圣经, 有效地与遗产代码工作

我对遗留代码和引入testing的一点经验就是创build“ 特征testing ”。 你开始用已知的input创buildtesting,然后得到输出。 这些testing对于你不知道他们真正做什么的方法/类很有用,但是你知道他们正在工作。

但是,有时几乎不可能创buildunit testing(甚至是特征testing)。 在这种情况下,我通过验收testing( Fitnesse )来解决问题。

你创build了一大堆所需的类来testing一个特性,并在fitnesse上进行检查。 这与“特性testing”类似,但是它高一级。

正如乔治所说的那样,遗产代码是有效的工作,是这类事情的圣经。

然而,团队中的其他人唯一的select就是如果他们亲自看到保持testing正常工作的好处。

为了达到这个目标,你需要一个尽可能容易使用的testing框架。 计划其他开发者将自己的testing作为例子来编写自己的testing。 如果他们没有unit testing经验,不要指望他们花时间学习一个框架,他们可能会看到编写unit testing会减慢开发速度,所以不知道框架是跳过testing的借口。

花一些时间用巡航控制,luntbuild,cdash等进行持续集成。如果你的代码每天晚上自动编译并运行testing,那么开发人员将开始看到unit testing在qa之前发现bug的好处。

有一点要鼓励的是共享代码的所有权。 如果开发者改变他们的代码并且打破了别人的testing,他们不应该期望那个人修复他们的testing,他们应该调查为什么testing不工作并且修理它自己。 根据我的经验,这是最难实现的事情之一。

大多数开发人员编写某种forms的unit testing,有时是一小部分他们没有签入或丢失的代码。 将这些内容融入到构build中,开发人员将开始购买。

我的方法是添加新的testing,并修改代码,有时你不能添加尽可能多的或作为详细的testing,而不需要解耦太多现有的代码,在实际的方面错误。

我坚持unit testing的唯一地方是平台特定的代码。 其中#ifdefsreplace为特定于平台的高级函数/类,必须在所有平台上使用相同的testing进行testing。 这可以节省添加新平台的时间。

我们使用boost :: test来构build我们的testing,简单的自我注册function使得编写testing变得简单。

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

我们每晚的构build是使用ant和luntbuild(ant粘c ++,.net和java构build)自动化的,

不久,我希望为构build添加自动部署和functiontesting。

我们正在做这个。 三年前,我join了开发团队的一个项目,没有unit testing,几乎没有代码评论和相当特别的构build过程。

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

第一步是添加一些unit testing。 不幸的是,这种行为是非常数据驱动的,所以在生成一个unit testing框架(最初是CppUnit,现在扩展到具有JUnit和NUnit的其他模块)方面做了一些初步工作,它使用了来自数据库的testing数据。 大多数初始testing是functiontesting,其中最外层是练习,而不是unit testing。 您可能不得不花费一些努力(您可能需要预算)来实施testing工具。

如果你增加unit testing的成本尽可能低,我发现它有很大的帮助。 testing框架使修复现有function中的错误时添加testing变得相对容易,新代码可以进行适当的unit testing。 当你重构和实现新的代码区域时,你可以添加适当的unit testing,testing更小的代码区域。

在去年,我们增加了与CruiseControl的持续集成,并使我们的构build过程自动化。 这增加了更多的激励,使testing保持最新和通过,这是早期的一个大问题。 所以我build议你包括定期(至less每晚)unit testing运行作为你的开发过程的一部分。

我们最近专注于改进我们的代码审查stream程,这是相当罕见和无效的。 目的是为了让开发人员更频繁地进行代码审查,从而使其更便宜。 另外,作为我们stream程改进的一部分,我试图让代码审查和unit testing包含在项目计划中的时间降低到一个更低的水平,以确保个人开发人员必须更多地考虑他们,而之前只有一个固定的比例在这个时间表上,他们更容易迷失方向。

我曾经在绿色领域项目中使用完全的unit testing代码库和大量的C ++应用程序,这些应用程序已经发展了很多年,并且拥有许多不同的开发人员。

老实说,我不打算试图获得一个遗留的代码库的状态,unit testing和testing第一次开发可以增加很多的价值。

一旦遗留的代码库达到了一定的规模和复杂性,使得unit testing覆盖率为您提供了许多好处后,就变成了等同于完全重写的任务。

主要的问题是,一旦你开始重构可testing性,你将开始引入错误。 只有一旦你得到高testing覆盖率,你可以期待所有这些新的错误被发现和修复。

这意味着你要么非常缓慢,要小心,直到几年后才能获得一个unit testing代码库的好处。 (可能从未发生合并等)。同时,您可能会引入一些新的错误,对软件的最终用户没有任何明显的价值。

或者你走得快,但有一个不稳定的代码库,直到你所有的代码达到高testing覆盖率。 (所以你最终有2个分支,一个在生产中,一个在unit testing版本。)

因为对于某些项目来说这个所有的规模问题都可能需要几周的时间才能完成。

需要考虑的一种方法是首先build立一个系统范围的仿真框架,用于开发集成testing。 从集成testing开始看起来可能与直觉相反,但是在所描述的环境中进行真正的unit testing的问题相当艰巨。 也许不仅仅是在软件中模拟整个运行时间…

这种方法只是绕过你列出的问题 – 尽pipe它会给你许多不同的问题。 但实际上,我发现使用强大的集成testing框架,您可以开发在单元级别运行function的testing,尽pipe没有单元隔离。

PS:考虑编写一个命令驱动的模拟框架,可能build立在Python或Tcl上。 这将让你脚本testing很容易…

天儿真好,

我首先看看任何明显的问题,例如在头文件中使用dec。

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

也许可以拿一本Jon Lakos的优秀着作“大规模C ++软件devise”( 清理亚马逊链接 )来获得应该如何布局的一些想法。

一旦你开始对代码库本身有点信心,比如文件布局中的代码布局,并且已经清除了一些不好的气味,例如在头文件中使用dec,那么你可以开始挑选一些你可以使用的function用来开始编写你的unit testing。

select一个好的平台,我喜欢CUnit和CPPUnit,并从那里去。

这将是一个漫长而缓慢的旅程。

HTH

干杯,

它更容易使其更加模块化。 你不能真正的unit testing有很多的依赖。 何时重构是一个棘手的计算。 你必须权衡成本和风险与收益。 这段代码是否会被广泛使用? 或者这个代码真的不会改变。 如果你打算继续使用它,那么你可能想要重构。

听起来像,但你想重构。 你需要从最简单的工具开始,并build立它们。 你有你的C模块,做一个gazillion事情。 也许,例如,有一些代码总是以某种方式格式化string。 也许这可以被看作是一个独立的实用程序模块。 你已经有了新的string格式化模块,你已经使代码更具可读性。 它已经是一个改进。 你是在断言你处于捕获状态。 你真的不是。 只要移动它,就可以使代码更具可读性和可维护性。

现在你可以为这个分解模块创build一个unit testing。 你可以通过几种方法来做到这一点。 您可以制作一个单独的应用程序,只包含您的代码,并在您的PC的主程序中运行一些案例,或者定义一个名为“UnitTest”的静态函数,执行所有testing用例,如果通过则返回“1”。 这可以在目标上运行。

也许你不能100%的用这种方法,但是这是一个开始,它可能会让你看到其他的东西,可以很容易地分解成可testing的工具。

我想,基本上你有两个单独的问题:

  1. 大代码基地重构
  2. 与一个团队合作

模块化,重构,插入unit testing等都是一项艰巨的任务,我怀疑任何工具都可能接pipe更大的工作。 这是一种难得的技巧。 一些程序员可以做得很好。 最讨厌它。

与一个团队做这样的任务是乏味的。 我强烈怀疑,“迫使”开发者将永远工作。 Iains的想法非常好,但是我会考虑find一两个能够并想要“清理”源代码的程序员:Refactor,Modualrize,引入unit testing等等。让这些人做这个工作,其他人介绍新的错误,aehmfunction。 只有喜欢这种工作的人才能胜任这项工作。

使testing容易。

我将开始把“自动运行”到位。 如果您希望开发人员(包括您自己)编写testing,可以轻松地运行testing,并查看结果。

编写一个三行的testing,对最新的版本运行,看到结果应该只有一个点击 ,而不是发送到咖啡机的开发。

这意味着你需要一个最新的版本,你可能需要改变人们如何处理代码等方面的政策。我知道这样的过程可以是embedded式设备的PITA,我不能给出任何build议。 但是我知道,如果运行testing是困难的,没有人会写他们。

testing什么可以testing

我知道我在这里运行普遍的unit testing的哲学,但这就是我所做的:为容易testing的事情编写testing。 我不打扰嘲笑,我不重构使其可testing,如果有UI介入,我没有一个unit testing。 但越来越多的我的图书馆例程有一个。

我很惊讶简单的testing往往发现。 摘下低垂的果实绝不是无用的。

用另一种方式来看待:如果不是一个成功的产品,你不会计划维持这个巨大的毛球。 您目前的质量控制不是完全需要更换的失败。 相反,使用unit testing,他们很容易做到。

(但是,你需要把它完成,不要被困在构build过程中的“修复所有东西”。)

教你如何改善你的代码库

任何具有这种历史的代码库都尖叫着改进,这是肯定的。 尽pipe如此,你永远不会重构所有的东西。

通过查看具有相同function的两段代码,大多数人可以同意在给定方面(性能,可读性,可维护性,可testing性)下的哪一个是“更好”的。 困难的部分是三个:

  • 如何平衡不同的方面
  • 如何认同这段代码足够好
  • 如何将不好的代码变成足够好的代码而不会破坏任何东西。

第一点可能是最困难的,同时也是工程问题。 但其他点可以学到。 我不知道有什么正式课程采用这种方法,但也许你可以组织一些内部的东西:任何东西从两个家伙一起到“研讨会”,你拿一个讨厌的代码,并讨论如何改善它。


这是一个哲学方面的一切。

你真的想要testing,function齐全,整洁的代码? 这是你的目标吗? 你从中得到什么好处?

是的,起初这听起来很愚蠢。 但说实话,除非你是系统的实际所有者,而不仅仅是一个员工,否则错误只是意味着更多的工作,更多的工作意味着更多的钱。 在做毛球的时候,你可以非常高兴。

我只是在这里猜测,但是,通过这场巨大的战斗,你所承担的风险可能远高于通过获得代码整齐可能的回报。 如果你缺乏社交技能来解决这个问题,你只会被视为一个麻烦制造者。 我见过这些人,而且我也是。 但是,当然,如果你真的做到这一点,那真是太棒了。 我会留下深刻的印象。

但是,如果你觉得现在为了维持一个不整洁的系统而花费额外的时间来欺负你,你真的认为一旦代码变得整洁和美观,情况就会改变。 不,一旦代码变得简洁,人们就可以在第一个可用的截止date之后,将所有这些空闲时间全部摧毁。

最后是pipe理层创造出漂亮的职场,而不是代码。