1. Introduction
1.1 Description
CUnit是一个用C语言编写、管理和运行单元测试的系统。CUnit以静态库的形式提供给用户使用,用户编写程序的时候直接链接此静态库就可以了。CUnit使用一个简单的框架来构建测试结构,并为测试常见数据类型提供了一组丰富的断言。此外,还提供了几个不同的接口用于运行测试和报告结果。其中包括用于代码控制测试和报告的自动化界面,以及允许用户动态运行测试和查看结果的交互式界面。
数据类型和函数在以下头文件中声明:
Header File | Description |
#include <CUnit/CUnit.h> | ASSERT macros for use in test cases, and includes other framework headers. |
#include <CUnit/CUError.h> | Error handing functions and data types. Included automatically by CUnit.h. |
#include <CUnit/TestDB.h> | Data type definitions and manipulation functions for the test registry, suites, and tests. Included automatically by CUnit.h. |
#include <CUnit/TestRun.h> | Data type definitions and functions for running tests and retrieving results. Included automatically by CUnit.h. |
#include <CUnit/Automated.h> | Automated interface with xml output. |
#include <CUnit/Basic.h> | Basic interface with non-interactive output to stdout. |
#include <CUnit/Console.h> | Interactive console interface. |
#include <CUnit/CUCurses.h> | Interactive console interface (*nix). |
#include <CUnit/Win.h> | Windows interface (not yet implemented). |
1.2 Structure
CUnit是一个独立于平台的框架与各种用户界面的组合。
用户界面便于与框架交互以运行测试和查看结果。CUnit提供了4种用户接口,每个接口有多个API可供使用。我们使用的是Basic接口,即默认输出到命令行,使用的API是CU_basic_set_mode()。使用Basic接口需要包含此头文件:#include "CUnit/Basic.h"
Interface | Platform | Description |
Automated | all | non-interactive with output to xml files |
Basic | all | non-interactive with optional output to stdout |
Console | all | interactive console mode under user control |
Curses | Linux/Unix | interactive curses mode under user control |
CUnit的核心框架为管理测试注册表,套件和测试用例提供了基本支持。CUnit的框架组织方式类似于传统的单元测试框架:
CUnit的测试是单线程启动,只能注册一个注册表(Test Registry), 一次测试可以运行多个套件(Test Suite),而每个套件可以包括多个测试用例(Test Case),每个测试用例又包含一个或者多个断言类的语句。具体到代码结构上,一个单元测试工程下包含多个Test Suite,它对应于程序中各个独立模块;一个Suite管理多个Test Case,它对应于模块内部函数实现。每个Suite可以含有setup和teardown函数,分别在执行suite的前后调用。
1.3 General Usage
使用CUnit框架的典型步骤顺序是:
- 为测试编写函数(必要时还可以使用init/cleanup套件)
- 初始化测试注册表:CU_Initialize_registry()
- 将套件添加到测试注册表:CU_Add_suite()
- 将测试添加到套件:CU_Add_test()
- 使用适当的接口运行测试,例如CU_basic_set_mode()
- 清理测试注册表:CU_Cleanup_registry()
2. Writing CUnit Test Cases
2.1 Stub
桩函数(Stub)实际上是白盒测试中的概念,它模拟被测试模块所调用的模块。如果被测试的单元模块需要调用其他模块中的功能或者函数(method),就应设计一个和被调用模块名称相同的桩模块来模拟被调用模块。这个桩模块本身不执行任何功能,仅在被调用时返回静态值来模拟被调用模块的行为。
为什么要使用桩函数?
- 加速开发。被替换的函数可能是目前还没写完的,这样能够加速开发,或更好的找错误源
- 减少测试的模块依赖。在测试模块A中发现问题,但不能确定是否因为调用的函数B引起时,我们使用桩函数来替代函数B,桩函数的功能与函数B的功能完全一致,必须得保证桩函数100%正确,这样我们就能排除是函数B的问题还是模块A的问题。
- 被测函数注入难以触发的输入。为了达到特定的目的替代原始函数,比如强制改变测试分支,将复杂业务简单化,而不是真实的去构造设置很多业务环境来达到条件,为了节省开支使用简单实用的办法直接替代。
//******************************************************************************
// @brief : 进行一个桩函数替换
// @param : void *Func : 待替换的目标函数
// @param : void *StubFunc: 桩函数
// @return :
// @note :
//******************************************************************************
int SimStub(void *Func, void *StubFunc);
//******************************************************************************
// @brief : 恢复被替换的桩函数
// @param : void *Func : 被替换的目标函数
// @return :
// @note :
//******************************************************************************
int SimStubReset(void *Func);
建议在test suite的开始建一个用于Init的test,里面可以调用SimStub完成替换,在suite结束建一个用于Teardown的test,里面调用SimStubReset恢复替换,这样做的好处是可以在suite层面完成替换和恢复替换,避免影响到其他suite。
2.2 Assertions
断言(Assert)用来判断值是否符合预期,从而判断是否发生了错误;CUnit提供了一组用于测试逻辑条件的断言,使用断言需要包含头文件:#include <CUnit/CUnit.h>。这些断言的成功或失败由框架跟踪,并且可以在测试运行完成时查看。
每个断言测试一个逻辑条件,如果该条件的计算结果为FALSE,则失败。失败后,除非用户选择断言"xxx_FATAL",否则测试将继续。以下是常用的断言:
断言 | 功能 |
CU_ASSERT_TRUE(value) | 断言值为True |
CU_ASSERT_FALSE(value) | 断言值为False |
CU_ASSERT_EQUAL(actual, expected) | 断言两值相等,注意这里比较的是int类型! |
CU_ASSERT_NOT_EQUAL(actual, expected) | 断言两值不等,注意这里比较的是int类型! |
CU_ASSERT_DOUBLE_EQUAL(actual, expected) | 断言两double值相等 |
CU_ASSERT_DOUBLE_NOT_EQUAL(actual, expected) | 断言两double值不等 |
CU_ASSERT_PTR_EQUAL(actual, expected) | 断言两指针值相等 |
CU_ASSERT_PTR_NOT_EQUAL(actual, expected) | 断言两指针值不等 |
CU_ASSERT_PTR_NULL(value) | 断言指针为NULL |
CU_ASSERT_PTR_NOT_NULL(value) | 断言指针不为NULL |
CU_ASSERT_STRING_EQUAL(actual, expected) | 断言两字符串相同 |
CU_ASSERT_STRING_NOT_EQUAL(actual, expected) | 断言两字符串不同 |
CU_PASS(message) | 断言进入该分支是正确的 |
CU_FAIL(message) | 断言进入该分支是不正确的 |
以下是两类特殊的断言
- xxx_FATAL
这种断言的含义是:出现这种值错误的后果非常严重,测试没有必要继续进行下去当FATAL断言失败的时候,会发生中断并立即返回,所以写在测试函数里面进行清理的代码段无法执行到,但是正常的套件清理功能不会受到影响。
- CU_PASS和CU_FAIL
CU_PASS和CU_FAIL不进行逻辑测试,仅用于测试是否运行到了正确的分支。
void test_longjmp(void)
{
jmp_buf buf;
int i;
i = setjmp(buf);
if (i == 0)
{
run_other_func();
CU_PASS("run_other_func() succeeded.");
}
else
{
CU_FAIL("run_other_func() issued longjmp.");
}
}
3. Managing Tests & Suites
用例和套件不能单独存在,用例(Case)需要注册到套件(Suite)中,套件需要注册到注册表(Test Registry)中。CUnit提供了两种注册方法:常规注册法(通过函数来注册)和快捷注册法(通过结构来注册)。下面介绍下快捷注册。它的主要好处是集中注册套件和相关测试,并最大限度地减少用户需要编写的错误检查代码。
(1)初始化一个CU_TestInfo类型(在<CUnit/TestDB.h>中定义)的数组,构成了套件。
每一行都表示一个在该套件中注册的用例:{用例名, 用例函数}。数组必须以包含NULL值的元素结尾,宏CU_TEST_INFO_NULL方便地定义了NULL值。
(2)初始化一个CU_SuiteInfo类型(在<CUnit/TestDB.h>中定义)的数组,构成了套件的集合。
每一行都表示一个套件:{套件名, 套件初始化函数, 套件清理函数, 用例的Setup函数, 用例的TearDown函数, 套件包含的用例数组}。通常,如果给定套件不需要NULL,则可以将其用于初始化或清理函数。数组必须以全NULL元素结尾,可以使用宏CU_SUITE_INFO_NNULL。
(3)调用CU_register_suites(suites)这个函数就可以完成所有用例和套件的注册。
typedef struct CU_TestInfo;
typedef struct CU_SuiteInfo;
//******************************************************************************
// @brief : 快捷注册,完成所有套件和用例的注册
// @param : CU_SuiteInfo suite_info[] : 套件集合
// @return :
// @note :
//******************************************************************************
CU_ErrorCode CU_register_suites(CU_SuiteInfo suite_info[]);
4. Running Tests
选中BEP_UT,右键点击"设为启动项目" -> 工具栏选择"X86" -> 工具栏"调试" -> "开始调试" or "开始执行"。
5. Coverage Check Tool
单元测试覆盖率可使用OpenCppCoverage插件。
5.1 安装
打开VisualStudio-->扩展-->管理扩展-->联机-->右上角输入OpenCppCoverage,在下面的结果会出现"OpenCppCoverage Plugin",点击安装即可,安装完成后需要重启VisualStudio。
如果下载过程出现进度条卡住几个小时不动 或 弹框“OpenCppCoverage Plugin:在web client请求期间发生异常”等,是VS2019服务器的问题。下载📎OpenCppCoverage-0.9.7.1.7z到本地,解压后安装即可正常使用(在工具栏或者选中工程点击右键可以看到该插件),如果Install过程下方出现End Task和Cancel两个选择框,点击End Task即可。
5.2 使用
程序写好编译执行之后,点击"工具-->Run OpenCppCoverage",程序运行,代码会出现红色或者绿色的阴影(绿色是覆盖的代码,红色是未覆盖的代码)如下图:
也可以运行OpenCppCoverage Settiings,设置.html报告生成的路径,如下图:
更多内容详见:CUnit - Table of Contents