实验五 单元测试
一、实验目的
1)掌握单元测试的方法
2) 学习XUnit测试原理及框架;
3)掌握使用测试框架进行单元测试的方法和过程。
二、实验内容与要求
1、了解单元测试的原理与框架
1.1 单元测试原理
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
单元测试的内容包括
模块接口测试、局部数据结构测试、路径测试、错误处理测试、边界测试
(1)模块接口测试
模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其他测试才有意义。模块接口测试也是集成测试的重点,这里进行的测试主要是为后面打好基础。测试接口正确与否应该考虑下列因素:
-输入的实际参数与形式参数的个数是否相同
-输入的实际参数与形式参数的属性是否匹配
-输入的实际参数与形式参数的量纲是否一致
-调用其他模块时所给实际参数的个数是否与被调模块的形参个数相同;
-调用其他模块时所给实际参数的属性是否与被调模块的形参属性匹配;
-调用其他模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
-调用预定义函数时所用参数的个数、属性和次序是否正确;
-是否存在与当前入口点无关的参数引用;
-是否修改了只读型参数;
-对全程变量的定义各模块是否一致;
-是否把某些约束作为参数传递。
如果模块功能包括外部输入输出,还应该考虑下列因素:
-文件属性是否正确;
-OPEN/CLOSE语句是否正确;
-格式说明与输入输出语句是否匹配;
-缓冲区大小与记录长度是否匹配;
-文件使用前是否已经打开;
-是否处理了文件尾;
-是否处理了输入/输出错误;
-输出信息中是否有文字性错误。
-局部数据结构测试;
-边界条件测试;
-模块中所有独立执行通路测试;
(2)局部数据结构测试
检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确,局部功能是整个功能运行的基础。重点是一些函数是否正确执行,内部是否运行正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
-不合适或不相容的类型说明;
-变量无初值;
-变量初始化或省缺值有错;
-不正确的变量名(拼错或不正确地截断);
-出现上溢、下溢和地址异常。
(3)边界条件测试
边界条件测试是单元测试中最重要的一项任务。众所周知,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现新的错误。边界条件测试是一项基础测试,也是后面系统测试中的功能测试的重点,边界测试执行的较好,可以大大提高程序健壮性。
(4)独立路径测试
在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。测试目的主要是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。具体做法就是程序员逐条调试语句。常见的错误包括:
-误解或用错了算符优先级;
-混合类型运算;
-变量初值错;
-精度不够;
-表达式符号错。
(5)错误处理测试
检查模块的错误处理功能是否包含有错误或缺陷。例如,是否拒绝不合理的输入;出错的描述是否难以理解、是否对错误定位有误、是否出错原因报告有误、是否对错误条件的处理不正确;在对错误处理之前错误条件是否已经引起系统的干预等。
通常单元测试在编码阶段进行。在源程序代码编制完成,经过评审和验证,确认没有语法错误之后,就开始进行单元测试的测试用例设计。利用设计文档,设计可以验证程序功能、找出程序错误的多个测试用例。对于每一组输入,应有预期的正确结果。
1.2 测试框架
xUnit是各种代码驱动测试框架的统称,这些框架可以测试 软件的不同内容(单元),比如函数和类。xUnit框架的主要优点是,它提供了一个自动化测试的解决方案。可以避免多次编写重复的测试代码。

底层是xUnit的framwork,xUnit的类库,提供了对外的功能方法、工具类、api等
TestCase(具体的测试用例)去使用framwork
TestCase执行后会有TestResult
使用TestSuite控制TestCase的组合
TestRunner执行器,负责执行case
TestListener过程监听,监听case成功失败以及数据结果,输出到结果报告中
Unit测试框架包括四个要素:
(1)测试目标(对象)
一组认定被测对象或被测程序单元测试成功的预定条件或预期结果的设定。Fixture就是被测试的目标,可以是一个函数、一组对象或一个对象。 测试人员在测试前应了解被测试的对象的功能或行为。
(2)测试集
测试集是一组测试用例,这些测试用例要求有相同的测试Fixture,以保证这些测试不会出现管理上的混乱。
(3)测试执行
单个单元测试的执行可以按下面的方式进行:
第一步 编写 setUp() 函数,目的是:建立针对被测试单元的独立测试环境;举个例子,这可能包含创建临时或代理的数据库、目录,再或者启动一个服务器进程。
第二步 编写所有测试用例的测试体或者测试程序;
第三步 编写tearDown()函数,目的是:无论测试成功还是失败,都将环境进行清理,以免影响后续的测试;
(4)断言
断言实际上就是验证被测程序在测试中的行为或状态的一个函数或者宏。断言的失败会引发异常,终止测试的执行。
1.3 面向特定语言的,基于xUnit框架的自动化测试框架
Junit : 主要测试用Java语言编写的代码
CPPunit:主要测试用C++语言编写的代码
unittest , PyUnit:主要测试用python语言编写的代码
MiniUnit: 主要用于测试C语言编写的代码
2、结对编程的小组采用测试框架 对自己“结对编程”实验的程序模块(类)进行单元测试,提交单元测试报告:
测试报告包括以下内容:
1)源码
2)测试用例设计 (结合单元测试的内容和模块功能设计测试用例)
3)选择的测试框架介绍、安装过程
4 )测试代码
5)测试结果与分析
3、push测试报告和测试代码到各自的github仓库
4、提交博客报告
三、实验过程
1.源码
把项目中的核心算法四则运算单独在另一个项目编译运行。
#pragma warning(disable : 4996) #include <stdio.h> #include <stdlib.h> #include <time.h> #include "miniunit.h" int main() { int a, b, d, t; //定义两个操作数a,b,结果d,输入结果t char c; //运算符c可取“+、-、×、÷” int i, sum = 0; //题目数量i,答对数目sum srand(time(0)); //初始化随机数发生器 for (i = 0; i < 10; i++) { a = rand() % 100 + 1; b = rand() % 100 + 1; c = rand() % 4; //0表加,1表减,2表乘,3表除 /**< 数据合格判断及算式显示 */ printf("第%d题:", i + 1); switch (c) { case 0: while ((d = a + b) > 100) //保证和在100内 { a = rand() % 100 + 1; b = rand() % 100 + 1; } printf("%d + %d = ", a, b); break; case 1: while (a > 100 || b > 100) //被减数小于100 { a = rand() % 100 + 1; b = rand() % 100 + 1; } if (a < b) //被减数大于减数 { d = a; a = b; b = d; } d = a - b; printf("%d - %d = ", a, b); break; case 2: while ((d = a * b) > 100) //保证积小于100 { a = rand() % 100 + 1; b = rand() % 100 + 1; } printf("%d × %d = ", a, b); break; case 3: while (a > 100 || b > 100 || (a * b == 0)) //保证被除数小于100且除数不为0 { a = rand() % 100 + 1; b = rand() % 100 + 1; } if (a < b) //被除数必须大于除数 { d = a; a = b; b = d; } a = (a / b) * b; //保证整除 d = a / b; printf("%d ÷ %d = ", a, b); break; } /**< 输入你的计算结果 */ scanf("%d", &t); if (d == t) { sum++; printf("正确\n"); } else printf("错误\n"); } /**< 输出答对题数和得分 */ printf("答对 %d 题,得分:%d\n", sum, sum * 10); return 0; }
可以运行,但是需要有一个单独的模块来用于单元测式。把计算过程和计算答对数量单独定义一个函数
#pragma warning(disable : 4996) #include <stdio.h> #include <stdlib.h> #include <time.h> #include "miniunit.h" int answerAndCount(int j) { int a, b, t ,i;//a\b\c随机数,t是正确答案 int sum=0;//答对数目sum int d;//结果 char c; //运算符c可取“+、-、×、÷” srand(time(0)); //初始化随机数发生器 for ( i = 0; i < j; i++) { a = rand() % 100 + 1; b = rand() % 100 + 1; c = rand() % 4; //0表加,1表减,2表乘,3表除 /**< 数据合格判断及算式显示 */ printf("第%d题:", i + 1); switch (c) { case 0: while ((d = a + b) > 100) //保证和在100内 { a = rand() % 100 + 1; b = rand() % 100 + 1; } printf("%d + %d = ", a, b); break; case 1: while (a > 100 || b > 100) //被减数小于100 { a = rand() % 100 + 1; b = rand() % 100 + 1; } if (a < b) //被减数大于减数 { d = a; a = b; b = d; } d = a - b; printf("%d - %d = ", a, b); break; case 2: while ((d = a * b) > 100) //保证积小于100 { a = rand() % 100 + 1; b = rand() % 100 + 1; } printf("%d × %d = ", a, b); break; case 3: while (a > 100 || b > 100 || (a * b == 0)) //保证被除数小于100且除数不为0 { a = rand() % 100 + 1; b = rand() % 100 + 1; } if (a < b) //被除数必须大于除数 { d = a; a = b; b = d; } a = (a / b) * b; //保证整除 d = a / b; printf("%d ÷ %d = ", a, b); break; } /**< 输入你的计算结果 */ scanf("%d", &t); if (d == t) { sum++; printf("正确\n"); } else printf("错误\n"); } return sum; } int main() { int i=0; //题目数量i, printf("请输入题目数量:"); scanf("%d", &i); int sum = answerAndCount(i); /**< 输出答对题数和得分 */ printf("每题十分,答对 %d 题,得分:%d\n", sum, sum * 10); system("pause"); return 0; }
通过编译运行并得到正确结果。

2.测试用例设计
| 编号 | 测试项 | 依赖用例 | 测试步骤 | 测试数据 | 预期结果 | 结果 |
| 1 | 答题数量 | 无 | 运行,输入答题数。根据步骤执行(无论对错)查看最终题目数量是否正确 | 10 | 10 | 10 |
| 2 | 答案正确性 | 无 | 运行输入答题数。根据步骤答对所有题目。检验其答案是否正确。 | 10 | 10 | 10 |
| 3 | 得分是否正确 | 无 | 运行输入答题数。答对前面的题目,答错最后一题。查看结果的分数是否正确。 | 10 | 90 | 90 |

如上图所示,用例测试1的结果

如上图所示,为用例测试2得结果

如上图所示,为用例测试3得结果。
3)选择的测试框架介绍、安装过程
源代码是用C语言所写的.cpp文件。因此选择miniunit来进行单元测试。
MinUnit 是一个极简的 C 语言单元测试框架,仅有三行代码,因此其功能亦比较受限。
与此相比,在同样保持简单的条件下,MiniUnit 提供更灵活的断言 (assertion),并以可读性更好的方式展示测试结果,包括显示断言错误的位置(文件名和行号),用彩色文本显示断言错误信息等(如下图)。
MiniUnit 的特点:
- 简单: 断言,测试,显示结果,没有额外的代码。
- 灵活: 灵活的断言,可选的消息,支持可变参数。
- 清晰: 显示错误位置(文件名和行号),支持彩色文本。
- 小巧: 仅有一个头文件,大约 120 行代码。
miniunit.h is a unit testing framework for pure-C. It features a simple interface for defining unit tests, putting little in the way of the developer.
- header-file only
- no external dependencies (only uses standard C libraries)
miniunit只包含头文件,并且没有外部依赖,只用到了标准c的库。只要添加miniunit.h头文件即可使用断言,编写并运行测试。
/** * miniunit.h * * Copyright (c) 2017 Hayato Takenaka * * Licensed under the MIT license: * https://opensource.org/licenses/mit-license.php **/ #pragma once #ifdef _MSC_VER # ifndef _CRT_SECURE_NO_WARNINGS # define _CRT_SECURE_NO_WARNINGS # endif #endif //------------------------------------------------------------------------------ // public //------------------------------------------------------------------------------ #include <stdlib.h> #include <stdbool.h> #ifdef _MSC_VER #else # include <pthread.h> #endif #define test_case(title, proc) do { \ miniunit_preproc((title)); \ double prev_time = miniunit_get_seconds(); \ (proc); \ miniunit_log(miniunit.colors.none, " Elapsed time %.6f sec", \ miniunit_get_seconds() - prev_time \ ); \ } while (false) #define expect(description, condition) do { \ bool satisfied = (condition); \ miniunit_lock(); \ { \ miniunit.item_count++; \ if (satisfied) { \ miniunit_log(miniunit.colors.green, \ " (%d-%d) [Passed] %s", \ miniunit.case_count, miniunit.item_count, (description) \ ); \ } else { \ miniunit_log(miniunit.colors.red, \ " (%d-%d) [Failed] %s (" #condition ") [%s:%d]", \ miniunit.case_count, miniunit.item_count, (description), \ __FILE__, __LINE__ \ ); \ } \ } \ miniunit_unlock(); \ if (!satisfied) { exit(1); } \ } while (false) //------------------------------------------------------------------------------ // private //------------------------------------------------------------------------------ typedef struct { // constants struct { int none, black, red, green, yellow, blue, magenta, cyan, white; } colors; char esc[9][8]; // internals struct lock_t { #ifdef _MSC_VER // TODO #else pthread_mutex_t mutex; #endif } lock; bool colorable; int case_count; char case_name[1024]; int item_count; } miniunit_t; extern void miniunit_preproc(const char* title); extern void miniunit_lock(); extern void miniunit_unlock(); extern double miniunit_get_seconds(); extern void miniunit_log(int color, const char* format, ...); extern miniunit_t miniunit; #ifdef MINIUNIT_MAIN #include <stdio.h> #include <string.h> #include <stdarg.h> #ifdef _MSC_VER # pragma comment(lib, "kernel32.lib") # include <windows.h> #else # include <time.h> # include <unistd.h> # include <pthread.h> #endif miniunit_t miniunit = { { 0, 1, 2, 3, 4, 5, 6, 7, 8 }, { "\x1b[0m", "\x1b[30m", "\x1b[31m", "\x1b[32m", "\x1b[33m", "\x1b[34m", "\x1b[35m", "\x1b[36m", "\x1b[37m" }, #ifdef _MSC_VER // TODO {} #else { PTHREAD_MUTEX_INITIALIZER } #endif }; void miniunit_preproc(const char* case_name) { miniunit_lock(); { miniunit.case_count++; miniunit.item_count = 0; strcpy(miniunit.case_name, case_name); miniunit_log(miniunit.colors.cyan, "(%d) %s", miniunit.case_count, case_name); } miniunit_unlock(); } void miniunit_lock() { #ifdef _MSC_VER // TODO #else pthread_mutex_lock(&miniunit.lock.mutex); #endif } void miniunit_unlock() { #ifdef _MSC_VER // TODO #else pthread_mutex_unlock(&miniunit.lock.mutex); #endif } void miniunit_log(int color, const char* format, ...) { char message[4096]; va_list args; va_start(args, format); vsprintf(message, format, args); va_end(args); #ifdef _MSC_VER // TODO color support for windows puts(message); #else if (isatty(fileno(stdout))) { printf("%s%s%s\n", miniunit.esc[color], message, miniunit.esc[0]); } else { puts(message); } #endif fflush(stdout); } double miniunit_get_seconds() { #ifdef _MSC_VER LARGE_INTEGER freq, now; QueryPerformanceFrequency(&freq); QueryPerformanceCounter(&now); return (double)now.QuadPart / freq.QuadPart; #else struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); return now.tv_sec + now.tv_nsec * 1e-9; #endif } #endif
如上所示,便是github上找到的一个miniunit头文件。
把该头文件添加到项目中去即可。
4.测试代码
example
#define MINIUNIT_MAIN #include "miniunit.h" int main() { test_case("Addition", { expect("1 + 1 = 2", (1 + 1) == 2); expect("1 + 1 + 1 = 3", (1 + 1 + 1) == 3); }); test_case("Subtraction", { expect("1 - 1 = 0", (1 - 1) == 0); expect("1 - 1 - 1 = -1", (1 - 1 - 1) == -1); expect("example", true == false); }); return 0; }
这是github上给出的示例。test_case是测试用例。expect为期待测试,当(1+1)==2为真时,正确,否则返回错误。
尝试写出我基于自己c语言程序代码的测试代码。先看测试用例的定义。
#define test_case(title, proc) do { \ miniunit_preproc((title)); \ double prev_time = miniunit_get_seconds(); \ (proc); \ miniunit_log(miniunit.colors.none, " Elapsed time %.6f sec", \ miniunit_get_seconds() - prev_time \ ); \ } while (false)
第一个是标题,应该是任意文本。
void miniunit_preproc(const char* case_name)
{
miniunit_lock();
{
miniunit.case_count++;
miniunit.item_count = 0;
strcpy(miniunit.case_name, case_name);
miniunit_log(miniunit.colors.cyan, "(%d) %s", miniunit.case_count, case_name);
}
miniunit_unlock();
}
再快速浏览定义可以看到以特定格式输出标题文本。
第二个很明显是直接执行,我也找不到定义。
test_case(title, proc) do{
...
(proc);
...
}while(false)
例子中proc是expect,再看expect(,)。
#define expect(description, condition) do { \ bool satisfied = (condition); \ miniunit_lock(); \ { \ miniunit.item_count++; \ if (satisfied) { \ miniunit_log(miniunit.colors.green, \ " (%d-%d) [Passed] %s", \ miniunit.case_count, miniunit.item_count, (description) \ ); \ } else { \ miniunit_log(miniunit.colors.red, \ " (%d-%d) [Failed] %s (" #condition ") [%s:%d]", \ miniunit.case_count, miniunit.item_count, (description), \ __FILE__, __LINE__ \ ); \ } \ } \ miniunit_unlock(); \ if (!satisfied) { exit(1); } \ } while (false)
第一个参数是描述问题
没找到bool satisfied = (condition);的相关定义,应该是满足条件的判定,这个条件应该是第二个参数。例子中,1+1==2这个逻辑判断式就是condition,(condition)的结果就是(1),即satisfied=(1),则按特定格式输出passed,如果condition这个式子结果为0,则按特定格式输出failed。再对第二个参数进行判定时,进程锁住,如果结果satisfied得出,并执行过if-else后解锁。
在勉强理解后开始写自己的测试代码。
test_case("实验五单元测试", {
expect("得分是否正确", (100 == answerAndCount(10)));
});
但是却报错了,e029:缺少)。我找不到原因在哪,完完全全符合格式,就算直接把例子复制上去也会这样报错。

从上图可以看到,完全符合参数,拓展依然是正确的,但是不知道为什么报错为错误表达式。我就按照定义将define的内容重新输入就不报错了。
do { miniunit_preproc(("实验五单元测试")); double prev_time = miniunit_get_seconds(); expect("得分是否正确", (100 == answerAndCount(10))); miniunit_log(miniunit_get_seconds(), "Elapsed time %.6f sec", miniunit_get_seconds() - prev_time); } while (false);
所以就用完整过程代替简化的#define test_case do{。。。}while(false)来使用。

所以测试代码为将远cpp文件中main函数的改进
int main() { int i = 0; //题目数量i, printf("请输入题目数量:"); scanf("%d", &i); //int sum = answerAndCount(i); //test_case("实验五单元测试", {expect("得分是否正确", (100 == answerAndCount(10)));}); do { miniunit_preproc(("实验五单元测试")); double prev_time = miniunit_get_seconds(); expect("得分是否正确", (100 == answerAndCount(i))); miniunit_log(miniunit_get_seconds(), "Elapsed time %.6f sec", miniunit_get_seconds() - prev_time); } while (false); /**< 输出答对题数和得分 */ //printf("每题十分,答对 %d 题,得分:%d\n", sum, sum * 10); system("pause"); return 0; }
输入题目数i=10,全部答对后,看得分是否等于100。
3.测试结果与分析
浙公网安备 33010602011771号