TDD-嵌入C语言的测试驱动开发
扫描二维码
随时随地手机看文章
要点
1.为什么你会遇上这些bug?因为它们是你放的。
2.在TDD(测试驱动的开发)中,你会在一个严格的反馈循环中,开发测试与生产代码。
3.TDD可能有助于避免恼人的Zune bug。
4.目标硬件瓶颈有多种形式,你可以在严格的TDD反馈循环中,用TDD来避开瓶颈。
5.TDD帮助你确保自己的代码如期望那样运行。但如果不是这样,你该如何建立一个可靠的系统?
6.TDD快速地发现小的和大的逻辑错误,防止出现bug,使最终得到较少的bug。
我们的工作方式都是编写代码,然后努力让它运行起来。先建立,然后改错。测试是以后的事,即写完代码后才要做的事。在不可预期的调试工作上,大概要花掉我们一半的时间。在日程表上,调试工作都穿着测试与集成的外衣。它是风险与不确定性的一个来源。修正了一个bug可能会产生另一个bug,有时甚至是一连串的bug。
保持调试的统计有助于预测要花多少时间才能消除bug。你要度量和管理bug。看曲线的拐点,拐点表示了趋势,告诉你最后修正的bug要比产生的多。拐点表示的是已经做的事,但你永远不知道是否在代码的某个阴暗角落还躲藏着其它的致命bug。
可制造性设计的一个方面是确定为什么你会有这些bug。答案很简单:错误是我们放进去的。这就是我们的工作方式。在开发以后的测试时,就会发现问题(图1和参考文献1)。我们在开发时会制造错误,测试的工作就是找到这些问题。只要仔细地测试,就会发现错误。开发后的测试工作意味着必须找到、修复和管理大量的错误。
图1,在开发以后做测试时,会发现缺陷
这种调试居后的编程程序是当今最常见的编程方式。先写代码,再调试它。调试居后的编程方式有风险。人都会犯错误。你既不能确定bug将在何时现身,也不能确定会花多长时间才能发现它们(图2)。
图2,人都会犯错误。你无法确定bug何时出现,以及要花多少时间才能找到它们
当发现一个bug的时间(TD)增加时,寻找bug根源的时间(TFIND)也会增加,通常增加得更多。如果从错误的引入到发现要花数小时、数天、数周,甚至数月时间,你已忘掉了当时的背景,必须开始做bug大扫荡。当你在开发周期以外发现缺陷时,就必须管理bug。对于有些bug,发现的时间不会影响修复的时间(TFIX),但有些代码的运行也可能依赖于bug,修改这些bug会造成其它bug。
短周期以及主动的测试自动化可节省时间和工作量。这时,你再不需要重复繁重而易错的手工测试。有了测试自动化,重复测试几乎不会增加额外工作量。测试自动化快速地探测出副作用,避免了对调试事务的需求。
另一种方案是TDD(测试驱动的开发),它在一个严格反馈的循环中开发出测试代码与生产代码(参考文献2和3)。一个TDD微循环是:编写一个测试,未编译时观察该测试,做编译且测试失败,使编译通过,清除任何多余内容,并重复该过程直至结束。编写测试代码与编写生产代码是整合的过程。如果犯了一个错误,没有通过新测试,你马上就可以知道并改正错误。测试会告诉你是否通过了新测试却产生了某个错误。在设备测试装置中加入自动化测试(图3),就可以自由地做重复测试。
图3,测试会告诉你是否通过了新的测试,但却引入了一个bug。自动测试要插入到一个单元测试装置中
在TDD反馈回路中做开发与测试时,只能避免一部分bug的出现,但不能完全消除。TDD对设计以及时间的分配方式有着意义深远的影响。
与后调试的编程模式相反,TDD并不包含追踪错误的风险与不确定性(图4)。当发现一个错误的时间接近于0时,寻找错误根源的时间也会趋于0。刚产生的代码问题通常显而易见。如果不那么明显,则开发人员只要简单地恢复刚做的修改,就可以回到一个可运行的系统。寻找和修改错误的时间和产生的时间一样少,只有当程序员记忆随时间而模糊,并且有更多的代码依赖于较早的错误时,事件才会变糟。
TDD为错误提供了即时的通知,可防止出现很多要被迫追踪的bug。TDD可防止出现缺陷,而后调试编程会带来耗时耗力的调试工作。
TDD可能有助于避免恼人的Zunebug。微软公司的Zune是为了与苹果公司的iPod竞争。2008年12月31日,Zune变成了“专为一天的程序块(abrick for a day)”。12月31日是新年前夜,是一个闰年的最后一天,这是30G Zune要经历的第一个闰年。很多人都将Zune错误归因于时钟驱动程序中的一个函数。虽然列表1中的代码并非实际的驱动程序码,但它有相同的效果。你可以从列表1中Zune的无限循环中找到一些端倪吗?
图4,TDD对于设计以及时间的使用有深远的影响。与调试居后的编程模式比较,TDD
没有回溯追踪bug的风险与不确定性
图5,对快速反馈的需求使TDD微循环离开目标硬件,而原生地运行在开发系统上。一个TDD循环包括双重目标的风险,但提供了快速TDD反馈回路的好处
很多代码阅读专家审查了这个代码,并得出了可能与您一样的错误结论。闫年的最后一天是该年第366天,而Zune对这种情况的处理是错误的。在这一天,该函数永远不会返回!我编写了设定年份以及年中天数的代码,看是否像90%的Zune bug专家预测的那样,将天数的布尔代码设定为等于或大于366就能解决问题。代码放入测试装置后,我编写了测试用例(列表2)。和Zune一样,测试进入了一个无限循环。我采用了经过数千名程序员审核的适当修复方法。出乎我的意料,测试失败了;设定年份与天数的测试认为日期是2009年1月0日。新年前夜,人们仍会拥有自己的音乐,但Zune仍有个bug。
一次测试就可以防止Zune bug。可你怎么知道要去写这样一个测试?只有知道bug在哪里才会写测试。问题是,你并不知道bug在哪里;它们可以在任何地方。所以,这意味着你必须为所有的部分写测试,至少是所有可能中断的地方。难以想象要考虑到所有需要测试的东西。但不必担心,你不需要针对全年每一天做测试。你只需要一个针对有关天数的测试。
计算机编程很复杂,TDD能够系统化地让你的代码按本意运行起来,并提供能使代码工作的自动化测试用例。
嵌入设计
当我首次使用TDD时,我认识到,它可能有助于解决一个问题:目标硬件的瓶颈,这是令很多嵌入软件开发人员头疼的事情。瓶颈有多种形式,你可以使用TDD,在严格的TDD反馈循环期间避免瓶颈的出现。很多嵌入开发工作都已实现了软硬件的并行开发。如果软件只能在目标硬件上运行,则可能浪费至少一次的时间。例如,目标硬件可能迟至交付期还不可用,推迟了软件的测试;硬件可能昂贵且稀少;或者它本身就有问题。目标硬件还可能有长的建立时间或长的上传时间。大多数嵌入开发团队都遇到过这些问题,它们会减缓进度,并减少了建立今天复杂系统的反馈。
为避免目标硬件的瓶颈,可以采用“双重目标”法,即设计自己的生产代码与测试,使之大部分运行在标准PC上。但双重目标有自己的风险。开发系统中测试代码的信任度是建立在交付给目标以前的代码上。大多数双重目标风险是源于开发环境与目标环境之间的差异。这些差异包括对语言特性支持的改变量、不同编译器的bug、运行时库的差异、文件名差异,以及不同的字长等。由于这些风险,你会发现,在一个环境下能无错运行的代码,可能在另一个环境下出现测试错误。
不过,执行环境中潜在的差异不应成为阻碍采用双重目标方法的理由。相反,你可以在实现目标的路途中解决这些障碍。嵌入TDD周期在不牺牲优点的前提下,克服了挑战。
开发循环
当建立与测试循环只需几秒时间时,TDD是最有效的。这种方案为大多数程序员排除了在循环中使用目标硬件的情况。快速反馈的需求将TDD微循环与目标分离开,而运行在开发系统上。图5显示了一个TDD循环,它包含着双重目标的风险,提供了快速TDD反馈循环的好处。
表1中所列的各个阶段,预计可以在相应的阶段发现问题。例如,你会发现每个阶段都有助于找到这些问题。第1阶段会在你编程时给出快速反馈,确定代码做你想要做的事。第2阶段确保你的代码是在两种环境下编译。第3阶段确保代码在主处理器和目标处理器上的运行相同。评估硬件可能需要比目标更多的存储器,这样才能把测试代码和生产代码都装入地址空间。有时候,如果你有一个可靠的目标硬件,它有空间运行对单元的测试,也可以省略掉第3阶段。第4阶段是在目标硬件上运行测试。在第4阶段可以引入一些依赖于硬件的单元测试。第5阶段是看你的系统完全整合时,是否如其应该的那样运行。至少让第5阶段的某些部分自动运行,这是一种好的想法。采用TDD的团队会发现第1阶段中的巨大价值,可能不要实现全部各个阶段。
嵌入TDD循环并不能阻止所有问题,不过它应有助于在适当的阶段发现大多数刚刚产生的问题。你还应至少每个夜晚手动执行第2至第4阶段。连续的集成服务器(如Cruise Control或Jenkins)都可以观察你的源码库,在check-in后开始做建立工作。
TDD有助于确保你的代码做你想要做的事。如果不是这样,如何才能建立一个可靠的系统呢?它帮助你让代码在最开始时保持正确,它建立一个逐步测试的组件,帮助你维持代码的运行。你在发现、追踪和修改bug上要花掉相当多的时间。很多开发人员现在都用TDD来防止这些bug的出现。它基本上改变了你的编程方式。
TDD能快速地发现小的和大的逻辑错误,阻止bug的产生,并最终得到较少的bug。较少的bug也意味着较少的调试时间,以及较少的缺陷。当新代码危及一个约束或一个假设时,测试会告诉你。然后,有良好结构的测试会成为一种形式的可执行文档。
TDD还让你放心,这种信心来自于一个带有完备回归测试组件的彻底测试代码。采用TDD的开发人员称周末不再受干扰,并且睡眠更好。TDD还监控进度,追踪当前的工作,以及做了多少工作。当代码变得难以测试时,它还对设计问题提出早期警告。