可以看出,在产品代码中我们嵌入了一段用于单元测试的代码,且通过UNIT_TESTING宏对这段代码的存在与否进行控制。读者可以认为这段代码与桩函数中的代码功能相似,但终达到的效果却有很大的不同。

  首先,UNIT_TESTING所控制的这段代码存在一个错误注入点,这个点以INJECTION_ POINT_DLL_POP_HEAD加以标识。从代码可以看出,该段代码先调用injected_error_get函数获取外部所注入的错误及数据。当外部没有错误注入时, dll_pop_head函数的功能与真正的产品代码是没有任何区别的(全多了一次对injected_error_get函数的调用),这相当于省去了我们在桩函数中编写dll_pop_head函数返回不为null的代码。

  单元测试难的部分是制造异常情形,比如让dll_pop_head函数返回null是我们测试mpool_buffer_alloc函数所需人为制造的。下面示例了新的单元测试程序是如何制造一个错误的。

    test_mpool.c
    void test_mpool_buffer_alloc ()
    {
        UNITEST_DIFFERS (mpool_buffer_alloc (handle), 0);
        error_inject (INJECTION_POINT_DLL_POP_HEAD,
            ERROR_T (ERROR_DLL_OUT_OF_NODE), null);
        UNITEST_EQUALS (mpool_buffer_alloc (handle), 0);
        error_inject (INJECTION_POINT_DLL_POP_HEAD, 0, null);
    }

  在这个新的单元测试程序中,我们不需为正常情形的测试做什么工作(这里做了一定形式的简化,实际的单元测试仍需要我们正常情形进行更为细致的检验),只是让dll_pop_head函数正常工作,返回不为null的节点行了。要让dll_pop_head函数返回null的话,人为地向INJECTION_POINT_DLL_POP_HEAD注入点通过error_inject函数注入错误。error_inject函数的第一个参数是注入点;第二个参数是所注入的错误码(我们约定0表示没有错误),该码作为调用injected_error_get函数的返回值;第三个参数是所需注入的数据,该数据通过injected_error_get函数的第二个参数返回。

  其次,在产品代码中由UNIT_TESTING宏所控制的代码并不需要有固定的格式,可以根据需要对错误点的行为进行定义。比如,可能对链表模块进行错误注入时,我们只希望影响链表1的行为,而不想对链表2有任何影响,这样的话可以将希望影响链表的指针注入到错误点中(此时需要定义一个数据结构,以便error_inject函数能传递多个参数),当然dll_pop_head函数中由UNIT_TESTING所控制的代码也得做相应的调整,使其在返回null前检查当前的链表指针(即_p_dll参数)与所注入的指针是否相同。

  为了能对象malloc这样的函数也增加错误注入点,我们需要对之进行封装。比如,提供osal_malloc函数,并在其中增加错误注入点所需的代码。采用这种方法的结果是,我们需要很薄的一个平台层。是否是跨平台完全取决于项目的特点。打个比方,对于基于VxWorks实时操作系统的项目,如果平台的编译开发是在Linux主机上的话,则所提供的平台层应实现跨VxWorks和Linux操作系统。千万不要忘了,此时单元测试应在Linux主机上完成,而非VxWorks上。而对于一个构建Linux应用软件的项目来说,如果开发也是在Linux主机上完成的话,平台层根本不需要考虑跨平台问题。

  引入平台层之后,读者或许会有两个担心:一是,跨平台库的开发需要大量的时间;二是,跨平台的代码又如何做单元测试?对于第一个担心,我的解释是:项目所使用的操作系统或C库的系统函数具有极强的收敛性,数目很有限,与传统单元测试所采用打桩的方式相比,我相信构建这样的平台层的工作量要小很多。至于第二个问题,我的建议是:由于平台层做得很薄,功能简单,我们可以放弃对之做单元测试。此时,单元测试的焦点应放在构建于平台之上的软件模块上。

  至此,错误注入方式的单元测试方法介绍完了。或许还有读者会问,这又不打桩,也将错误点处理代码嵌入在产品代码中,这还是单元测试吗?实际上,单元测试的目的是为了让被测代码的各行和各分支能运行到以确保质量,只要达到这一目的,是否打桩或在产品代码中嵌入测试代码并非介定是不是单元测试的关键条件。我们应时刻记住,开发活动中的行为应是价值驱动的,而非形式驱动的。一个没有价值的行式,注定是浪费,也一定会束缚思想让我们在困境中难以找到突破的途径。价值驱动的观点往往会让我们抛开束缚,获得意想不到的效果。

  后,整理一下以错误注入这一方法实施单元测试的特点:

  1)引入很薄的平台层。在平台层中增加错误注入点处理代码,以便更好地对构建于之上的软件模块实施单元测试。时否跨平台取决于项目,但放弃对这一平台层进行单元测试。

  2)构建于平台之上的各软件模块,在合适的地方也增加错误注入点处理代码,以方便对其他依赖于它的模块实施单元测试。

  3)由于不需要额外的桩代码,所以代码的维护开销更低,且天然具备复用的特点。

  4)由于产品代码与测试代码进行了一定的合并,更容易做到“无缝整合”(参见《单元测试实施解惑(一)》)