1. 单元测试的必要性
单元测试是软件开发的重要一环, 尤其对嵌入式开发. 因为嵌入式开发受限于开发环境, 调试工具等因素, 不能和纯 PC 软件开发一样使用很多先进的工具. 这就需要开发者在开发过程中, 进行更细的模块划分, 更明确的接口, 更详尽的测试. 根据软件工程理论, 1 个 bug 越是在后期越是花费巨大的成本去修复, 并且随着系统复杂度的增长, 在一个大的系统中去查找某一个细节具体的问题, 相比于在小的模块中去查找问题会花费多倍的时间成本.
2. 单元测试框架解剖
一般地单元测试需要实现以下几个基本功能:
1. assert
各种 assert, 比如 AssertTrue, 比如 AssertFail,AssertStrEquals,AssertIntEquals......
条条大路通罗马, 这些 Assert 有各种功能, 其实就是包装了断言的函数. 比如 AssertStrEquals(str, "open"), 进行 str 和字符串 "open" 的比较, 如果不相同则会报错.
根据框架的结构, 在 assert 失败时候, 有的进行长跳转 longjmp, 有的对类似 failCount 的全局的变量进行加 1 并记录错误位置.
2. 错误位置记录
得益于 C 语言的 LINE,FILE 宏, 这是 2 个 ANSI C 标志支持的内置宏定义, 可以得到当前的的行数和文件名. 在断言失败的地方, 记录文件名和行号, 以供用户查询错误的位置.
- char buf[HUGE_STRING_LEN];
- sprintf(buf, "%s:%d:", _FILE_, _LINE_);
3. 测试 case 管理
这是测试框架区别于自己写的 assert 测试函数最根本的地方. 测试框架为了提高函数利用率, 减少重复, 方便测试例程汇总等, 都会进行各种封装. 比如以下几条.
1)setup 和 teardown
大部分的测试框架都提供这两个函数, 主要是因为有些测试 case, 有大量重复的代码, 比如准备输入数据, 测试完毕后清理现场等通用的功能.
2)测试例子汇总
有的叫做 TestSuit, 有的叫做 TestFixtures. 把一类相似功能的测试 case 进行汇总, 方便更高层次的调用, 也方便用户管理测试例程.
3)测试的调用
多个测试例程汇总后, 构成一个数组(表格), 启动运行, 一般由 xxxRun 函数负责.
在嵌入式 c 中, 一般都有一个函数指针来操作, 这也是为什么所有的测试 case 的函数名称都使用相同的声明, test_case 需要和调用该测试的指针同类型.
4. 测试的执行
测试的执行本质就是函数的长跳转. 可以看做在父函数中调用子函数, 这个子函数如果是测试例程的话, 子函数就会包含 assert 相关的语句, 而 assert 语句在出错后, 会记录错位位置和错误消息, 然后进行长跳转 (longjmp),longjmp 和 setjmp(buf) 成对出现, 返回到调用的位置, 然后进行下一个测试 case.
- for (i = 0 ; i <testSuite->count ; ++i)
- {
- Test* testCase = testSuite->list[i];
- TestRun(testCase);
- if (testCase->failed) {
- testSuite->failCount += 1;
- }
- }
3. 测试框架的本质
1)为了更好的组织测试, 提供的测试组的批量处理功能, 一般由 for 循环遍历一个 table 数组实现;
2)为了减少重复进行测公用函数提取, 比如准备测试环境和清理现场;
3)测试需要的各种断言;
4)断言失败后的跳转, 记录错误位置 - FILE-, -LINE - 宏的使用;
5)测试 case 运行的监控和结果的汇总.
综上, 如果你实现了上面的几个功能, 那么也就自己完成了一个测试框架.
其实测试框架是一个很简单的事情, 如今测试框架有很多, 像 VS 这样的 IDE 已经集成了单体测试, 所以对于一个开发者怎么规划测试才是测试工作的第一要务.
如何恰当的写测试用例, 既不延误开发又不会造成工程臃肿, 还能尽可能的覆盖测试范围, 这才是测试中最花费功夫的地方.
来源: https://www.cnblogs.com/pingwen/p/9206406.html