单元测试 – stub – 桩 – mock – 模拟
1. 单元测试
单元测试 (unit testing) 是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,要根据实际情况去具体含义,C 语言中单元指一个函数,Java 中单元指一个类,图形化软件中可以指一个窗口或一个菜单等。单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
与单元测试联系起来的一些开发活动包括代码走读 (code review)、静态分析 (static analysis) 和动态分析 (dynamic analysis)。
- 静态分析是对软件的源代码进行研读,查找错误或收集一些度量数据,并不需要对代码进行编译和执行。
- 动态分析是通过观察软件运行时的动作,来提供执行跟踪,时间分析,以及测试覆盖度方面的信息。
单元测试是由程序员自己来完成,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
2. stub – 桩
桩代码就是用来代替某些代码的代码。单元测试应避免编写桩代码。
产品函数或测试函数调用了一个未编写的函数,可以编写桩函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码。这样做的好处是减少了工作量,测试上层函数时,也是对下层函数的间接测试。当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。
桩函数 (stub) 是模拟被测试模块所调用的模块。桩或桩代码是指用来代替关联代码或者未实现代码的代码。如果函数 F 用 F1 来代替,F 称为原函数,F1 称为桩函数。打桩就是编写或生成桩代码。
打桩的目的有隔离、补齐、控制:
- 隔离是指将测试任务从产品项目中分离出来,使之能够独立编译、链接,并独立运行。隔离的基本方法就是打桩,将测试任务之外的,并且与测试任务相关的代码,用桩来代替,从而实现分离测试任务。例如函数 A 调用了函数 B,函数 B 又调用了函数 C 和 D。如果函数 B 用桩来代替,函数 A 就可以完全割断与函数 C 和 D 的关系。
- 补齐是指用桩来代替未实现的代码。例如,函数 A 调用了函数 B,而函数 B 由其他程序员编写,且未实现。可以用桩来代替函数 B,使函数 A 能够运行并测试。补齐在并行开发中很常用。
- 控制是指在测试时,人为设定相关代码的行为,使之符合测试需求。
一般来说,桩函数要具有与原函数完全一致的原型,仅仅是实现不同,这样测试代码才能正确链接到桩函数。
- 用于实现隔离和补齐的桩函数一般比较简单,只需把原函数的声明拷过来,加一个空的实现,能通过编译链接就可以。
- 比较复杂的是实现控制功能的桩函数,要根据测试的需要,输出合适的数据。
是一个功能强大的自动化 C/C++ 单元级测试工具,可以自动测试任何 C/C++ 函数、类,自动生成测试用例、测试驱动函数或桩函数,在自动化的环境下极其容易快速的将单元级的测试覆盖率达到 100%。
- 接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上,通过这种方式,我们的代码将完全与接口的实现分离,使得我们可以透明地将某个实现替换为另一个实现。
- 利用 C 编译器的预编译特点,通过宏定义替换需要打桩的函数。
- 修改函数内存地址,通过 jump 指令跳转到 stub 函数
3. mock – 模拟
mock 测试是在测试过程中,对于某些不容易构造或不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。虚拟的对象就是 mock 对象。mock 对象就是真实对象在调试期间的代替品。
使用范畴:
真实对象具有不可确定的行为,产生不可预测的效果 (股票行情,天气预 )。
真实对象很难被创建。
真实对象的某些行为很难被触发。
真实对象实际上还不存在。

mock 可以用来模拟 stub。
stub 关注交互行为,为了验证被测系统调用目标系统接口的交互行为。
mock 关注状态,为了验证调用目标系统后,目标系统的状态。
4. interposition – 打桩
库打桩 (interposition) 是由 Linux 链接器提供的技术,允许用户截获对共享库函数的调用,并执行自己的代码 (当然是在普通权限下,管理员权限通常是禁止使用该技术的)。使用打桩机制,可以追踪某个特殊库函数的调用次数、验证并追踪其输入输出,甚至把它替换成一个完全不同的实现。
给定需要打桩的目标函数,常见一个 wrapper 函数,其原型和目标函数一致。利用特殊的打桩机制,可以实现让系统调用你的 wrapper 函数而不是目标函数。wrapper 函数中通常会执行自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。
打桩可以发生在编译时、链接时或者程序被加载执行的运行时。不同的阶段都有对应的打桩机制,也有其局限性。基本目标是用打桩来追踪程序运行时对函数的调用。
编译时打桩需要访问程序的源代码,连接时打桩需要能够访问程序的可重定位的对象文件。运行时打桩仅需要访问可执行目标文件即可,它的基本原理是基于动态链接器的 环境变量的。如果 环境变量被设置为一个共享库路径的列表 (以空格或分 分隔),那么当你加载和执行一个程序,需要解析未定义的引用时,动态链接器会先搜索 中给定的库,然后才搜索任何其他的库。有了这个机制,当你加载和执行任意可执行文件时,可以对任何共享库中任意函数打桩。
References
CUnit – A unit testing framework for C
http://cunit.sourceforge.net/doc/index.html
A lightweight library to simplify and generalize the process of writing unit tests for C applications.
https://github.com/google/cmockery
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!