3.4 写第一个测试
现在测试列表已经有了,我们可以开始了。很自然,第一个测试是去测试初始化是否正确。LED在初始化后应当全部关闭。
首先我们要建立LedDriver测试文件。按照惯例,可以将它命名为LedDriverTest.c。我通常把测试代码放在一个与产品代码不同的目录里。我会把这些代码放在unity/LedDriver目录中,并调整makefile从而让它能编译和链接这个新的测试文件。给测试起个合适的名字来反映我们要实现的目标,这个文件看起来如下所示:
现在让测试检查一些有意义的东西。看一下测试列表,驱动程序的一个职责是在初始化时把所有的LED都关闭。
如何进行检查呢?自动化测试没办法查看LED,不是吗?这需要有光感设备或者光电设备才行,不是吗?的确,在软硬件集成时我们要去看LED,但是在单元测试时我们可以虚拟地来看它们。
在目标硬件上有一个特定的地址——一个I/O内存映射地址,真正地连接到电路上。写到这个地址中的比特决定了是打开还是关闭特定的LED。在调用LedDriver_Create()时驱动程序必须把一个16比特的0值写进LED的I/O地址中,以便把所有的LED关闭。
这是一本关于TDD的书,因此另一个设计目标是LedDriver必须独立于硬件可测(也就是说在开发环境中可测)。如果在测试过程中驱动程序要向目标硬件的物理地址中写入数据,这会是个问题:内存崩溃或内存访问失败。让我们来重定位这个地址,看看如何让这段代码在开发环境中可测。
为驱动程序伪造环境
如果这个地址是外界传给驱动程序的,那么在测试用例中就可以用有一系列虚拟LED的地址来伪造成真正的物理地址。所谓虚拟LED无非就是一个与LED内存映射有着同样比特位数的变量。测试用例可以设置、重置以及读取代表这个虚拟LED的变量。驱动程序不会知道它被捉弄了。它只是按部就班地如同操作内存映射设备一样打开RAM中的一个比特。
虚拟LED工作起来没有问题,但我们要检查什么值呢?设计说明和测试列表会指引我们要检查什么值。设置某一个LED位为0会把这个LED关闭,设置为1会把它打开。硬件启动时每个LED都处于打开状态。按照设计说明,由软件负责在初始化时把所有的LED关闭。所以我们最好能保证虚拟LED的每一位都被置为0。
为了测试LED是否被正确初始化,我们要写以下测试:
这里有个很微妙的地方。在TEST(LedDriver, LedsOffAfterCreate)中virtualLeds先被置为0xFFFF,随后调用了LedDriver_Create(&virtualLeds)。把virtualLeds初始化成0xFFFF确保了测试可以把vitualLeds碰巧为0和被正确地初始化为0区分开。
并且,请注意virtualLeds的类型。这里用一个16位无符号整数表示LED,与LED在I/O空间内存映射的宽度匹配。测试和产品代码须要至少在两个机器上运行:开发系统和目标设备。结果是,virtualLeds的宽度必须明确指定。如你所知,int的大小在不同架构的机器上是不一样的。使用可移植的整型,如stdint.h中的uint16_t,能让我们强制把该整型的长度在任何机器上都指定为16位。
如我们期望的一样,编译会出错:
依赖注入
把virtualLeds传给驱动程序的做法称作“依赖注入”(dependency injection)。并不是让驱动程序在编译时知道并依赖于LED的地址,而是我们在运行时将其注入。只有目标系统的初始化函数在编译时依赖LED物理地址。
使用依赖注入的一个有意义的附带好处是LedDriver的可重用性更好了。可以将驱动程序放到库中让别的有着不同LED地址的系统使用。这同时也是一个TDD自然地引导出灵活设计的例子。
不要让编码跑到测试前面
如果你在设想最终的代码,这种不完整的实现可能会让你很困惑。并没有将ledsAddress保存在任何地方,很明显它必须保存在某个地方。至少现在还不要保存它,因为还没有一个失败的测试要求保存它。这需要写出一个测试,除非你把地址保存下来,否则就会失败。下一个测试会迫使驱动程序使用前面传入的ledsAddress。
我知道忍住不去写那些你已经知道会用到的代码是很难的,但还是不要写。让编码跟在测试之后,坚持这样的原则就能产生被全面的测试用例彻底测试过的产品代码。
Bob Martin编写的“TDD三条原则”给我们提供了一个如何在写测试代码和产品代码之间切换的指导。在有测试要求保存地址之前就这样做,这将违反Bob的TDD三条原则。