测试驱动开发系列之二--嵌入式系统TDD策略

嵌入式系统TDD策略

这篇文章中主要介绍嵌入式TDD周期和如何不受跨平台问题影响而保持开发的步伐。我们看到了双目标系统的优势和如何包容在目标系统中测试所带来的风险。然后也解答了一些常见的关于TDD的疑惑。

5.1目标硬件的瓶颈
对于很多嵌入式项目来讲,并行进行硬件和软件开发是个现实。如果开发的软件只能在目标硬件上运行,你很可能会遭遇到下面的一个或多个浪费时间的因素。
直到项目的后期硬件还没有就绪,只能推迟软件测试。
目标硬件贵而稀缺。这会让开发人员去等,并且还要在大量未验证的工作之上去创造新的内容
当目标硬件终于可以用了,它自己可能也有BUG。一大堆没有测试过的软件也有BUG。要日复一日的调试,并且会有很多的互相指责。
在编辑-编译-加载-测试的循环中宝贵的时间浪费在漫长的上传上和目标系统构建上。
通常来讲为目标硬件工作的编译器都要比本地编译器贵得多。开发团队的可用授权数量可能很有限,这会增加成本并可能会造成更多的延迟。
传统意义上,这也是嵌入式开发者会转而使用评估板来缓解目标硬件瓶颈的一个原因。但这绝对不够。评估板仍然有构建时间长和上传时间长的问题,但它的确提供了一个相对便宜的平台。

5.2双目标开发的好处
双目标意味着从第一天起,你的代码就被设计成至少要在两个平台上运行:最终的目标系统硬件和你的开发系统。
双目标解决了一下几个问题。它让你能在硬件就绪之前就测试代码,并且使它在整个软件开发周期里避免硬件带来的瓶颈。
双目标如TDD一样有另一个好处,它会影响你的设计。对软件与硬件之间边界的关注会产生更模块化的设计。
一个以测试为目的,由双目标开发附带来的一个好处就是代码将来会更容易移植到不同的硬件平台上。硬件改变总是发生,而且常常失去控制。从双目标的代码开始,可能刚好让它在下次向意料之外的目标硬件平台移植时简单些。

5.3双目标测试的风险
编译器所支持的语言特征可能不同
目标编译器可能有一组BUG,而开发系统中的本地编译器有另一组BUG
运行时库可能不一样
头文件和其功能可能不同
基本数据类型可能大小不同
字节序和数据结构对齐可能不同
由于存在这些风险,你会发现可能在一个环境里运行没有错误的代码再另一个环境里却测试失败。

5.4嵌入式的TDD循环
在构建和测试的循环中只有几秒钟的情况下TDD效果最好。更长的构建时间和测试时间会导致采用更大的步伐。随着更大的步伐而来的是有更多的东西可能被破坏,从而导致在最终运行测试时需要更多的调试。
平台1:TDD微循环
第一个平台运行得最频繁,通常几分钟一次。在这个平台上你要写于平台无关的代码。你要去寻找吧软件和硬件断开的机会,只要可行,越多越好。硬件和软件的边界要很清楚,并且记录在测试用例中。
平台2:编译器兼容性检查
要定期地为目标系统做编译,采用为产品而使用的交叉编译器。这个平台是对编译器不兼容的一个早期警告的机会。它会警告我们的移植问题。但不必每次代码改变时都运行平台2.应该在每次采用了新的语言特征时来做一下目标系统的交叉编译。
平台3:在评估板上运行单元测试
编译后的代码再本地开发系统和目标处理器上运行起来是不同的。使用评估板可以看到代码在开发系统和目标处理器上行为的差异。
这些测试的运行应当被作为持续集成构建的一部分,至少每天运行。
平台4:在目标硬件上运行单元测试
这些测试可以识别出或者学习到目标硬件的行为。这个平台一个新增的挑战是目标硬件上有限的内存。
平台5:在目标硬件上运行验收测试
最后在目标硬件上运行自动化的手工的验收测试来保证产品特征。要确保任何不能完全被自动化测试的依赖于硬件的代码都会被手工测试。无论目标硬件是否可靠,代码的主体仍旧要在平台1--脱离开目标硬件做TDD中写出和测试。

5.5 双目标的不兼容性
我们可能会碰到下面的移植性的问题
1.运行时库也有BUG
2.不兼容的头文件

5.6和硬件一起测试
在任何可行的地方,和硬件一起测试都应该自动化。让我们来看看3种我们可能会创建的和硬件打交道的测试。
1.自动化硬件测试
当硬件改动不可避免的发生时,你的测试会帮你看到新的硬件设计是否有问题。你可能会发现其中的一些测试在生产中也有价值。在没有硬件的前提下,还要写一个硬件的模拟器来让测试通过。一开始硬件验收测试只是为软件团队带来好处。一段时间后,电气工程师也开始越来越信任我们的测试了。硬件验收测试时的电气工程师可以升级他们的工具组,重新编译他们的设计,并且有信心他们不会破坏任何东西而且充满自信的增加新的功能。
2.部分自动化硬件测试
如果我们能有效地最小化依赖于硬件的代码,极有可能依赖于硬件的代码不需要经常改动。要确保最后一寸代码也就是紧挨着硬件的代码,是正确的。
3.利用外部设备的自动化硬件测试
专门的测试设备可以帮助自动化硬件测试。

持续集成CI(Continuous Integration)
持续集成是测试驱动开发的伙伴实践。,成功的CI需要有自动构建。构建系统要足够简单。CI服务器会监控代码库的签入并在签入完成后触发一个完整的构建和测试过程。一个嵌入式系统构建由两个阶段完成,首先为开发系统做测试。然后是为目标系统构建。
CI是一个避免风险的策略,还是节省时间的利器。当开发人员在没有集成的情况下走了很久,集成的难度和风险都在增加。代码合并要很小,并且要辅以自动化测试,自动化测试则由TDD产生。

对于TDD策略的疑惑
6.1我们没那个时间
第一个可以挤出时间做TDD的地方是你现在的实践。你应当能够把一些用于被动调试的时间转换为主动做TDD的方式。这种加速来源于减少了现在和以后的调试时间,以及由测试来作为可执行文档的更清晰的代码库。

手动测试
在手动测试上的初始投资可能比自动化测试要低些,但它不可持续,它在将来的回报率接近为零。对于手动测试过的代码的改动将使得之前的手动测试无效。

定制自动化测试框架
这些测试很有帮助,他们改进了代码质量,但是在集成之后,测试便荒废,因为测试工作已经转移到系统集成那边去了。测试不再与代码同步,投资回报率也就随之减少了。定制打造的测试main函数也会比用CPPUTest这样的自动化单元测试框架要花更多工夫来写测试。

单步跟踪式的单体测试
这不仅慢而且注定就是一个不可重复的过程。这些测试的保质期比main函数测试还差。任何一点改动都会让前面的测试无效。你必须重新再做一遍。

文档化和评审过的单元测试流程
流程包括首先要文档化单元测试过程,其次该过程要经过评审和批准。最后他们还要记录下执行该流程的证据。缺点就是遮掩的努力往往投入产出比很小。当重复手动单元测试流程时,流程变得很无聊,自然就会走捷径,BUG就溜进了代码中。

你花在单元测试上的真金白银区哪里了
请考虑吧你花在单元测试上的工夫从你当前的流程中转化一些到TDD上来吧,有了TDD,每次改动所有的测试都会运行,测试与代码同时演进,并且这样的投入将换来加倍的回报。

TDD会影响设计。当后测的时候你不会得到同样多的正面的设计影响,TDD会产出更好的API和高内聚低耦合的模块
TDD会避免缺陷发生,当你犯一点小错误,TDD会马上发现它们。
当后写测试的时候,你还必须花时间来寻找测试失败的根本原因,而在TDD中根本原因往往很明显。
TDD更有活力,它提供了更好的测试覆盖率--也是正确的测试覆盖率。测试覆盖率不是TDD的目的,但对于在代码之后写测试来讲测试覆盖率很难得到保证。

6.3测试也需要维护
测试要保持整洁,有表现力并且不能有重复。学会这些技巧要花时间。当你在TDD和测试用例设计上熟练起来,你会发现测试不一定非要做得那么难维护。

6.4单元测试不能发现所有的BUG
TDD不能避免所有的BUG,但这不能成为不做TDD的理由。TDD是在通过每一行代码都按我们的期望工作起来来帮助建造相当牢固的积木块。TDD会消除大量的问题从而使更高层次的测试能发现恰当的问题。TDD也许不能避免所有的BUG,但它却可以非常有效地避免错误变成BUG。

6.5我们的构建时间太长
为了达到TDD的节奏,需要快速的增量构建--不需要为TDD构建整个系统。在管理依赖关系时,可以只独立构建系统的一部分。真正的挑战在于要有模块化的代码。

6.6我们有现存的代码
给遗留代码(没有测试的代码)建议的策略是一边产出新的产品功能,一边增量地添加测试。
用TDD来开发新的函数和模块
在修改已有代码时为它增加测试
当修改BUG时增加测试。
有先见性地投入在一些有策略意义的测试上。

6.7我们的内存有约束
在开发系统中运行测试不会揭露出目标系统中的内存约束问题。这里有些方法可以帮助在内存有约束的情况下做TDD。
使用双目标,从而使代码主体可以在目标系统之外测试。
找一个小一点的测试框架。
做一个实验室版本的目标系统,让它有足够的内存装下所有的产品代码和测试用例。
创建多个测试容器,每个都有测试的一个子集让它可以装进有限的内存中。
监视目标构建的内存使用情况。持续集成系统可能会在构建目标闪存二进制映像的同时创建一个映射文件。一个简单的Shell脚本就可以读取这个映射文件,以便计算出代码区域的使用情况。

6.8我们不得不和硬件交互
与硬件交互的测试也可以在目标硬件之外写出和测试。用代码模拟特定的交互场景比模拟一连串甚至整个东西要简单得多且高效得多。

6.9为什么要用C++的测试框架来测试C 大部分的CPPUTest都可以用标准的C语言来写,但标准的C不能直接处理测试用例的自安装。让每个测试都需要多个入口使得测试的重构更难。如果重命名或者拆分一个测试就要做重复的工作。C++有一个重要特征,它提供了对语言内置的对象初始化的支持。声明一个文件范围的对象,它会在main函数运行之前用对象的构建函数初始化。构建函数就像是结构体的初始化函数。基本上来讲,TEST()宏会创建一个文件作用域的C++对象,它的构建函数会把这个TEST()安装到所有测试的列表中。 即使目标系统只有C编译器,我也不想放弃CppUTest。当你用不同的编译器来编译时,你会发现一些你以前不会发现的问题。在本地开发系统中测试可以帮助发现可移植性的问题。自安装的测试可以帮助消除测试用例丢失的问题。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结