本篇文章决定采用 TDD 的方式开发一个 LED 的 C 语言驱动程序来说明 TDD 的整个开发流程。
什么样的 C 模块可以进行单元测试?
C 语言是面向过程的语言,不像 C++ 和 Java 这类面向对象的编程语言。要想对代码进行单元测试,一个比较好的方法是对代码进行模块化的设计。好的模块化设计,不仅能够满足单元测试的需要,而且还能增加代码的维护性以及复用性。
根据 C 语言的特点,我把代码的接口放置在 C 语言的头文件(.h)中,代码的具体实现放置在代码的源文件(.c)里。考虑到驱动在运行时是否可以拥有多个运行实例,在头文件中的接口设计时应该加以考虑。
使用 TDD 创建 C 模块是,遵循以下的几个惯例会更方便:
- 用头文件来定义模块的接口;
- 源文件用来包含对接口的实现;
- 测试文件用来包含测试用例,以保证测试的代码和产品代码分离;
- 模块初始化及清理函数;
驱动的功能设计
在正式编码前,首先需要对 LED 驱动的功能进行一个大概的功能设计。在实际的开发过程中,不同的驱动有着不同的功能需求,我在这里就直接将《测试驱动的嵌入式C语言开发》书中的驱动功能需求抄过来:
- 控制 16 个有两个状态的 LED;
- 可以在不影响其他 LED 的情况下打开或者关闭任意一个 LED;
- 通过单一的测试接口调用来打开所有的 LED;
- LED 驱动程序的用户可以查询任何 LED 状态;
- 当加电时,硬件的默认状态是所有的 LED 都锁定在打开状态,需要由软件来吧它们关闭;
- LED 会在内存中映射到一个 16 位的字上(在一个尚未确定的地址上);
- 在某一位上置 1 会点亮对应的 LED;置 0 则会将之关闭;
- 最小一位对应 LED 1,最大一位对应 LED 16;
测试用例表
明确了具体的需求后,TDD 的第一步不是编写代码,而是根据需求,首先完成测试用例表的编写。这里需要注意的一点是在创建测试用例列表时的报酬递减(最开始能够明确的测试的项目,越往后能够想到的新增的测试用例越有限)。由于“报酬递减”的原则,所以在编写测试用例列表时不需要花费太多的时间。
测试列表可以根据不同的公司的软件开发文档表格进行编写。我这里就直接随便在文章中做相应的编写即可。
LED 驱动的测试用例列表
1 | 在驱动程序安装后关闭所有的 LED |
编写测试用例
完成了测试用例列表后,我们就可以进行第一个测试用例的编写了。在测试用例的编写时,需要遵循自己的工程选择的单元测试框架。再次采用的单元测试框架是前面提到过的 Unity 自动化单元测试框架。
第一个测试
首先,建立测试文件 LedDriverTest.c 如下:
1 |
|
其他的文件在此就就不明确进行说明了。
到此,就完成了第一个测试。通过这个测试,就构建好了一个自动化的单元测试环境。我们可以在这个环境上对 LED 的驱动程序进开发。
为驱动程序伪造环境
由于 LED 驱动在生产环境中需要硬件的支持,我们在实际开发的过程中不可能等硬件完成之后再开始软件的开发工作。因此,LED 的驱动开发过程中,我们需要为 LED 的驱动程序伪造一个运行环境,也就是采用某些策略来减少软件对硬件的依赖。
1 | TEST(LedDriver, LedsOffAfterCreate) |
上面代码中的 virtualLeds 就是软件在进行单元测试时进行虚拟的 LED 驱动。将 virtualLeds 传给驱动程序的做法称作“注入依赖”。
完成以上编程后,对整个项目进行构建,然后运行。到此还未创建 LedDriver.h 和 LedDriver.c 文件。构建必然是失败的。这时,我们编写的测试就开始驱动我们进行了驱动开发的第一步——创建 LedDriver 驱动的头文件和源文件。
创建好了对应的头文件和源文件后,在头文件中完成 LED 驱动模块的接口的编写,在原文件中的实现可以先写成空函数。
这里需要牢记的一个原则就是:不要让编码跑到测试的前面。
Bob Martin 编写的“TDD 三条原则”给我们提供了一个图和在写测试代码和产品代码之间切换的指导。
- 除非是让一个失败的单元测试通过,否则不要写产品代码;
- 不要写比足以失败更多的单元测试,构建失败也不可以;
- 不要写比足以让单元测试通过更多的产品代码;
到此,我们开始了 TDD 开发模式的第一步。