参考:
关于EXPECT_CALL
关于TEST的断言宏
关于gtest的C语言封装
gtest+stub打桩
stub工具源码:https://github.com/coolxv/cpp-stub/tree/master/src
0. 背景/需求说明:
0.1. 需求
- C语言的项目需要做单元测试(需要满足基本的测试,如返回值,字符串比较等)
- 涉及多个模块的交互(需要做隔离,打桩,自制输入)
- 实现打桩,函数替换进行测试(要求测试不能影响被测代码,不能修改被测代码,即函数指针,宏定义修改的方法被禁止,且由于是C语言,不具备C++的多态性和继承)
0.2. 方法
综上考虑,采用googletest+stub的方法进行单元测试。
- googletest虽为C++设计,但也能满足C的基本测试要求,且足够成熟。
- 舍弃googlemock,采用stub。stub通过修改函数地址的方法实现函数替换,能满足要求。
关于gmock和stub的详细区别,以及stub的使用。懒,等以后再写。(很多大佬也写了)
0.3. 构建
通过Cmake加入项目即可。懒,等以后再写。(同样很多大佬也写了)
1. gtest文件层级划分与说明
- 第一层:按模块划分
如test_MonitorAlarm文件夹,test_log文件夹 - 第二层:模块内按功能划分
如test_MonitorAlarm文件夹中的test_read.cpp,test_check.cpp - 第三层:模块内各功能的具体测试
如test_read.cpp中的各种TEST(){}
1.1. 单元测试运行方法
首先当然是cmake和make。
-
当想运行所有模块单元测试时。
在 build 文件夹中直接make test
,便会利用ctest运行所有模块的单元测试 -
当想运行单个模块的单元测试时。(包含显示每个具体测试的结果)
进入 build/gtest/bin/ 文件夹中运行相应模块的测试进程。
2. 新增单元测试
2.1. 新增模块测试
- 在gtest文件夹中新建模块文件夹,其中包含CMakeLists.txt,main文件,各功能测试文件。
(可直接复制模板文件夹,再修改CMakeLists.txt的project名即可) - 在gtest的CMakeLists.txt中add_subdirectiry(新增模块)。
2.2. 新增模块功能测试
- 直接新建.cpp文件
- include gtest/gtest.h和gmock/gmock.h头文件(使用stub,要加stub.h)
- using namespace testing命名空间说明
注意事项:
-
一定要.cpp结尾
因为Googletest主要是针对C++的单元测试(当然,C语言项目也能使用)。 - 一定要using namespace testing(要是不用gmock,不加也行)
3. 单元测试具体实现
主要包括两方面:gtest和gmock
3.1. gtest
gtest主要包括两种方式:TEST和TEST_F
两者区别,TEST宏的作用是创建一个简单测试,而TEST_F宏用于在多个测试中使用同样的数据配置,所以它又叫 测试夹具(Test Fixtures)每个测试都将从测试类做派生。
由于TEST_F更多是用在C++上,因此本单元测试主要使用TEST。
TEST(分类名, 测试名) {
测试代码
}
通过测试下面add_int函数,举例说明
int add_int(int x,int y){
return x+y;
}
想要测试add模块的add_int功能。如下
//对于add_int函数的测试 分类为TestAddInt,具体创建三个测试,分别测试输入值的三种情况
//输入值同为正数(测试名为add_int_positive)
TEST(TestAddInt, add_int_positive) {
int ret=add_int(10,24);
EXPECT_EQ(ret,34);
}
//输入值同为负数(测试名为add_int_negative)
TEST(TestAddInt, add_int_negative) {
int ret=add_int(-10,-24);
EXPECT_EQ(ret,-34);
}
//输入值一正一负(测试名为add_int_nega_posi)
TEST(TestAddInt, add_int_nega_posi) {
int ret=add_int(-10,24);
EXPECT_EQ(ret,14);
}
//通过调用add_int函数,并利用EXPECT_EQ比较输出结果是否符合预期,从而达到测试目的
其中的EXPECT_EQ为断言的宏,Gtest中,断言的宏可以理解为分为两类:
- ASSERT_* 系列,当检查点失败时,退出当前函数(注意:并非退出当前案例)。
- EXPECT_* 系列,当检查点失败时,继续执行下一个检查点(每一个断言表示一个测试点)
通常情况应该首选使用EXPECT_,因为ASSERT_*在报告完错误后不会进行清理工作,有可能导致内存泄露问题
常用的断言宏有以下六类(具体请查看附录)
- 布尔型检查
- 二值检查
- 浮点检查
- 相近值检查
- 字符串检查
- 异常检查 (不适用,因为要使用C++的throw,鸡肋)
3.2. gmock(C项目推荐用stub)
mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。
如:在进行单元测试时,我们想要测试自己的函数A
,但是函数A
却依赖于函数B
,当函数B
无法满足预期时就无法对函数A
进行测试,主要由于下面几个原因:
-
函数B
依赖于硬件设备 - 真实的
函数B
的返回值无法满足我们的预期 - 团队开发中
函数B
尚未实现
这时就需要对函数B
进行打桩(仿真mock),使其达到我们预期的效果。
而这时就涉及到了函数的多态。C++可以在不修改函数A
的情况下,通过虚函数实现动态多态,将调用函数B
修改为调用仿真mock函数。而C语言不支持虚函数。
因此,需要手动将函数A
调用函数B
,通过函数指针,修改为调用仿真mock函数。
因为会对代码进行改动,所以在利用gmock做单元测试时,建议能少用就少用,可以自己在测试函数中模拟数据。
真要用的时候,记得在测试完成后,将改动的代码恢复原样。
例子如下:
//函数A,调用函数B去处理num
int test_A(int num){
if(test_B(num)<=100){
return 0;
}
return 1;
}
//函数B处理num
int test_B(int num){
num+=50;
return num;
}
1.先构建结构体,包装输入函数B的参数以及函数指针
typedef struct ctest{
int num;//输入函数B的参数
int (*p_test_B)(struct ctest*);//指针指向 返回值为int型,参数为ctest类型指针 的函数
}ctest;
2.修改函数A中的函数调用,将函数B替换为函数指针
int test_A(ctest* p_c_struct){//将参数修改为结构体ctest
if(p_c_struct->p_test_B(p_c_struct->num)<=100){//修改函数的调用
return 0;
}
return 1;
}
上述两个步骤,需要在源码中进行修改!!!!!!!!
后续步骤在测试文件中
3.创建mock类,并定义mock方法
class Mock_FOO{
public:
//定义mock方法
MOCK_METHOD1(mock_test_B,int (ctest* p_c_struct));
//其中MOCK_METHOD1最后的数字1代表参数有一个,同理若要定义一个无参数的mock方法,则MOCK_METHOD0
//mock_test_B为定义的mock函数名
//int 表示该函数返回值
//ctest* p_c_struct表示该函数的参数
};
4.实例化mock对象,并创建mock对象方法的函数的C包装
//实例化mock对象
Mock_FOO mocker;
//创建mock对象方法的函数的C包装
int mock_test_B(ctest* p_c_struct){
return mocker.mock_test_B(p_c_struct);
}
5.创建测试例子
TEST(TestMock, test_mock) {
EXPECT_CALL(mocker, mock_test_B(An<ctest *>())).WillRepeatedly(Return (1000));
//EXPECT_CALL是mock最主要的部分,配合.WillRepeatedly等限制可以实现在调用该mock函数时,自定义模拟各种效果!
//这句的最终效果即为,每当test_A调用mock_test_B时,都会返回1000
//具体EXPECT_CALL解释请看附录!!!
ctest c_struct_foo;
c_struct_foo.p_test_B = mock_test_B;
int ret=test_A(&c_struct_foo);
EXPECT_EQ(ret,1);
}
具体EXPECT_CALL的介绍请看附录!!!!!!
3.3. stub
如某个待测函数get_date(),需要调用另一个模块的my_read函数。
int get_date(){
int rc = 0;
rc = my_read()
return rc;
}
为了测试get_date函数,需要打桩,将my_read函数替换自制的桩函数。自己设置返回的值
int stub_my_read(){
return 0;
}
打桩操作如下
int stub_my_read(){
return 0;
}
TEST(Test_get_date,success){
Stub s_get_date; //创建Stub对象
s_get_date.set(my_read,stub_my_read); //通过set,将my_read函数替换为桩函数stub_my_read()。
int rc = 0;
rc = get_date(); //当程序运行到get_date函数内的my_read函数时,会跳转到执行桩函数stub_my_read()
EXPECT_EQ(rc, 0);
}
4. 附录
4.1. 常用的断言宏
4.1.1. 布尔型检查
Nonfatal assertion | Verifie |
---|---|
EXPECT_TRUE(condition); | condition is true |
EXPECT_FALSE(condition); | condition is false |
4.1.2. 二值检查
Nonfatal assertion | Verifie |
---|---|
EXPECT_EQ(expected, actual); | expected == actual |
EXPECT_NE(val1, val2); | val1 != val2 |
EXPECT_LT(val1, val2); | val1 < val2 |
EXPECT_LE(val1, val2); | val1 <= val2 |
EXPECT_GT(val1, val2); | val1 > val2 |
EXPECT_GE(val1, val2); | val1 >= val2 |
4.1.3. 浮点检查
Nonfatal assertion | Verifie |
---|---|
EXPECT_FLOAT_EQ(expected, actual); | the two float values are almost equal |
EXPECT_DOUBLE_EQ(expected, actual); | the two double values are almost equal |
在对比数据方面,我们往往会讨论到浮点数的对比。因为在一些情况下,浮点数的计算精度将影响对比结果。“几乎相等”是指两个值彼此相差 4 个 ULP
4.1.4. 相近值检查
Nonfatal assertion | Verifie |
---|---|
EXPECT_NEAR(val1, val2, abs_error); | the difference between val1 and val2 doesn’t exceed the given absolute error |
例子如下
//测试10+24=34,而36是否在误差5的范围内
TEST(TestNEAR, test_NEAR) {
int ret=add_int(10,24);
EXPECT_NEAR(ret,36,5);
}
注:整型时包含边界,浮点时因为精度问题,不包含边界
如EXPECT_NEAR(34,36,2);//测试成功
如EXPECT_NEAR(34.1 , 34.2 ,0.1);//测试失败
4.1.5. 字符串检查
Nonfatal assertion | Verifie |
---|---|
EXPECT_STREQ(expected_str,actual_str); | the two C strings have the same content |
EXPECT_STRNE(str1, str2); | the two C strings have different content |
EXPECT_STRCASEEQ(expected_str, actual_str); | the two C strings have the same content, ignoring case (忽略大小写) |
EXPECT_STRCASENE(str1, str2); | the two C strings have different content, ignoring case (忽略大小写) |
STREQ和STRNE同时支持char和wchar_t类型的,STRCASEEQ和STRCASENE却只接收char*,例子如下
char* test_char( ){
char* a="hi";
return a;
}
//测试test_char返回值是否为"hi"
TEST(TestChar, test_char_EQ) {
char* b=test_char();
EXPECT_STREQ(b,"hi");
}
//忽略大小写
TEST(TestChar, test_char_CASEEQ) {
char* b=test_char();
EXPECT_STRCASEEQ(b,"Hi");
}
4.2. EXPECT_CALL的具体介绍
EXPECT_CALL(mock_object, method(matcher1, matcher2, ...))
.With(multi_argument_matcher)
.Times(cardinality)
.InSequence(sequences)
.After(expectations)
.WillOnce(action)
.WillRepeatedly(action)
.RetiresOnSaturation();
第1行的mock_object就是Mock类的对象
第1行的method(matcher1, matcher2, …)中的method就是Mock类中的某个方法名,比如上述的mock_test_B;而matcher(匹配器)的意思是定义方法参数的类型,后面详细介绍。
第3行的Times(cardinality)的意思是之前定义的method运行几次。至于cardinality的定义,也会在后面详细介绍。
第4行的InSequence(sequences)的意思是定义这个方法被执行顺序(优先级),后面举例说明。
第6行WillOnce(action)是定义一次调用时所产生的行为,比如定义该方法返回怎么样的值等等。
第7行WillRepeatedly(action)的意思是缺省/重复行为。
结合该3.2中gmock的TEST:EXPECT_CALL(mocker, mock_test_B(An<ctest *>())).WillRepeatedly(Return (1000));
//EXPECT_CALL(mocker, mock_test_B(An<ctest *>()))
//mocker即为mock对象
//mock_test_B即为创建的mock方法。
//An<ctest *>()为匹配器,其中的An表示可以是type类型的任意值,在这里表示ctest *类型的任意值
//.WillRepeatedly(Return (1000))
//表示每次调用时都会返回1000
4.2.1. 匹配器(matcher)
用于定义Mock类中的方法的形参的值
包含以下几种类型
- 通用
匹配器 | 解释 |
---|---|
_ | 可以代表任意类型 |
A() or An() | 可以是type类型的任意值 |
- 一般比较
匹配器 | 解释 |
---|---|
Eq(value) 或者 value | argument == value,method中的形参必须是value |
Ge(value) | argument >= value,method中的形参必须大于等于value |
Gt(value) | argument > value |
Le(value) | argument <= value |
Lt(value) | argument < value |
Ne(value) | argument != value |
IsNull() | method的形参必须是NULL指针 |
NotNull() | argument is a non-null pointer |
Ref(variable) | 形参是variable的引用 |
TypedEq(value) | 形参的类型必须是type类型,而且值必须是value |
- 浮点数的比较
匹配器 | 解释 |
---|---|
DoubleEq(a_double) | 形参是一个double类型,比如值近似于a_double,两个NaN是不相等的 |
FloatEq(a_float) | 同上,只不过类型是float |
NanSensitiveDoubleEq(a_double) | 形参是一个double类型,比如值近似于a_double,两个NaN是相等的,这个是用户所希望的方式 |
NanSensitiveFloatEq(a_float) | 同上,只不过形参是float |
- 字符串匹配
这里的字符串即可以是C风格的字符串,也可以是C++风格的。
匹配器 | 解释 |
---|---|
ContainsRegex(string) | 形参匹配给定的正则表达式 |
EndsWith(suffix) | 形参以suffix截尾 |
HasSubstr(string) | 形参有string这个子串 |
MatchesRegex(string) | 从第一个字符到最后一个字符都完全匹配给定的正则表达式. |
StartsWith(prefix) | 形参以prefix开始 |
StrCaseEq(string) | 参数等于string,并且忽略大小写 |
StrCaseNe(string) | 参数不是string,并且忽略大小写 |
StrEq(string) | 参数等于string |
StrNe(string) | 参数不等于string |
-
容器的匹配
关于STL,这里就不说明了。
4.2.2. 基数(Cardinalities)
基数用于Times()中来指定模拟函数将被调用多少次文章来源:https://www.toymoban.com/news/detail-430052.html
基数 | 解释 |
---|---|
AnyNumber() | 函数可以被调用任意次. |
AtLeast(n) | 预计至少调用n次. |
AtMost(n) | 预计至多调用n次. |
Between(m, n) | 预计调用次数在m和n(包括n)之间. |
Exactly(n) 或 n | 预计精确调用n次. 特别是, 当n为0时,函数应该永远不被调用. |
4.2.3. 行为(Actions)
Actions(行为)用于指定Mock类的方法所期望模拟的行为:比如返回什么样的值、对引用、指针赋上怎么样个值,等等。文章来源地址https://www.toymoban.com/news/detail-430052.html
- 值的返回
行为 | 解释 |
---|---|
Return() | 让Mock方法返回一个void结果 |
Return(value) | 返回值value |
ReturnNull() | 返回一个NULL指针 |
ReturnRef(variable) | 返回variable的引用. |
ReturnPointee(ptr) | 返回一个指向ptr的指针 |
- 分配值
行为 | 解释 |
---|---|
Assign(&variable, value) | 将value分配给variable |
- 使用函数或者函数对象(Functor)作为行为
行为 | 解释 |
---|---|
Invoke(f) | 使用模拟函数的参数调用f, 这里的f可以是全局/静态函数或函数对象. |
Invoke(object_pointer, &class::method) | 使用模拟函数的参数调用object_pointer对象的mothod方法. |
- 复合动作
行为 | 解释 |
---|---|
DoAll(a1, a2, …, an) | 每次发动时执行a1到an的所有动作. |
IgnoreResult(a) | 执行动作a并忽略它的返回值. a不能返回void. |
到了这里,关于(笔记)googletest在C语言项目中的使用指南的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!