(笔记)googletest在C语言项目中的使用指南

2023-10-29


参考:
关于EXPECT_CALL
关于TEST的断言宏
关于gtest的C语言封装
gtest+stub打桩
stub工具源码:https://github.com/coolxv/cpp-stub/tree/master/src

0. 背景/需求说明:

0.1. 需求

  1. C语言的项目需要做单元测试(需要满足基本的测试,如返回值,字符串比较等)
  2. 涉及多个模块的交互(需要做隔离,打桩,自制输入)
  3. 实现打桩,函数替换进行测试(要求测试不能影响被测代码,不能修改被测代码,即函数指针,宏定义修改的方法被禁止,且由于是C语言,不具备C++的多态性和继承)

0.2. 方法

综上考虑,采用googletest+stub的方法进行单元测试。

  1. googletest虽为C++设计,但也能满足C的基本测试要求,且足够成熟。
  2. 舍弃googlemock,采用stub。stub通过修改函数地址的方法实现函数替换,能满足要求。
    关于gmock和stub的详细区别,以及stub的使用。懒,等以后再写。(很多大佬也写了)

0.3. 构建

通过Cmake加入项目即可。懒,等以后再写。(同样很多大佬也写了)

1. gtest文件层级划分与说明

  1. 第一层:按模块划分
    如test_MonitorAlarm文件夹,test_log文件夹
  2. 第二层:模块内按功能划分
    如test_MonitorAlarm文件夹中的test_read.cpp,test_check.cpp
  3. 第三层:模块内各功能的具体测试
    如test_read.cpp中的各种TEST(){}

1.1. 单元测试运行方法

首先当然是cmakemake

  • 当想运行所有模块单元测试时
    build 文件夹中直接make test,便会利用ctest运行所有模块的单元测试
  • 当想运行单个模块的单元测试时。(包含显示每个具体测试的结果)
    进入 build/gtest/bin/ 文件夹中运行相应模块的测试进程。

2. 新增单元测试

2.1. 新增模块测试

  1. 在gtest文件夹中新建模块文件夹,其中包含CMakeLists.txt,main文件,各功能测试文件。
    可直接复制模板文件夹,再修改CMakeLists.txt的project名即可
  2. 在gtest的CMakeLists.txt中add_subdirectiry(新增模块)。

2.2. 新增模块功能测试

  • 直接新建.cpp文件
  • include gtest/gtest.h和gmock/gmock.h头文件(使用stub,要加stub.h)
  • using namespace testing命名空间说明

注意事项:

  1. 一定要.cpp结尾
    因为Googletest主要是针对C++的单元测试(当然,C语言项目也能使用)。
  2. 一定要using namespace testing(要是不用gmock,不加也行)

3. 单元测试具体实现

主要包括两方面:gtestgmock

3.1. gtest

gtest主要包括两种方式:TESTTEST_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,365);
}

注:整型时包含边界,浮点时因为精度问题,不包含边界
如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 (忽略大小写)

STREQSTRNE同时支持char和wchar_t类型的,STRCASEEQSTRCASENE却只接收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)的意思是定义这个方法被执行顺序(优先级),后面举例说明。
第6WillOnce(action)是定义一次调用时所产生的行为,比如定义该方法返回怎么样的值等等。
第7WillRepeatedly(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()中来指定模拟函数将被调用多少次

基数 解释
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类的方法所期望模拟的行为:比如返回什么样的值、对引用、指针赋上怎么样个值,等等。

  • 值的返回
行为 解释
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.
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

(笔记)googletest在C语言项目中的使用指南 的相关文章

  • Qt自定义开关按钮控件

    Qt自定义开关按钮控件 最近的项目需要在页面中添加一个开关按钮 样式类似于iOS的wifi开关按钮 在网上借鉴了别人的代码 稍作修改可以呈现出想要的效果 代码如下 switchcontrol h include
  • leetcode 326. Power of Three(3的次方)

    问题描述 Given an integer write a function to determine if it is a power of three Follow up Could you do it without using an
  • Linux进程

    目录 1 进程 1 1 什么是进程 1 2 如何来描述进程 PCB process control block 1 3 task struct 1 4 如何查看进程 1 5 获取标识符 1 6 如何创建一个进程呢 1 7 进程的状态 1 7
  • 5705. 判断国际象棋棋盘中一个格子的颜色

    给你一个坐标 coordinates 它是一个字符串 表示国际象棋棋盘中一个格子的坐标 下图是国际象棋棋盘示意图 如果所给格子的颜色是白色 请你返回 true 如果是黑色 请返回 false 给定坐标一定代表国际象棋棋盘上一个存在的格子 坐
  • VMware Workstation Player安装CentOS 7和基本配置

    本文主要分享使用VMware Workstation Player 15安装CentOS 7的过程 最后还有CentOS 7的基本设置 1 环境准备 下载安装VMware VMware Workstation Player 15 下载Cen

随机推荐

  • Hive 单表列行转换和多表列行转换

    一 单表列行转换 描述 表中记录了各年份各部门的平均绩效考核成绩 表名 t1 表结构 a 年份 b 部门 c 绩效得分 表内容 a b c 2014 B 9 2015 A 8 2014 A 10 2015 B 7 多行转多列 问题1 将上述
  • 51单片机模块化编程

    一 传统方式编程 所有的函数均放在main c 里 若使用的模块比较多 则一个文件内会有很多的代码 不利于代码的组织和管理 而且很影响编程者的思路 模块化编程 把各个模块的代码放在不同的 c 文件里 在 h 文件里提供外部可调用函数的声明
  • 无法解决 equal to 运算中 \"Chinese_PRC_CI_AS\" 和 \"Chinese_PRC_CS_AS\" 之间的排序规则冲突。

    无法解决 equal to 运算中 Chinese PRC CI AS 和 Chinese PRC CS AS 之间的排序规则冲突 之所以会出现这种错误 是因为服务器不能用不同的排序规则来比较两段文本 但是发果用collate关键字显式的创
  • linux驱动arm蜂鸣器响,ARM11 硬件 PWM驱动蜂鸣器设备代码

    include include include include include include include include include include include include include include include
  • C++基础知识 - 函数模板

    函数模板 C 提供了模板 template 编程的概念 所谓模板 实际上是建立一个通用函数或类 其类内部的类型和函数的形参类型不具体指定 用一个虚拟的类型来代表 这种通用的方式称为模板 模板是泛型编程的基础 泛型编程即以一种独立于任何特定类
  • 前端面试题收集整合

    面试题 一 在css布局中 什么场景下出现元素高度塌陷 如何解决元素高度塌陷问题 父元素的所有子元素设置浮动后会出现元素高度塌陷问题 1 父元素设置高度 2 父元素设置浮动 3 修改父元素的类型 display inline block t
  • 【JAVA】Parameter 0 0f OSTrto co.damlr.mtfcti.vicMotifictonservice meined ben f tp “SnramwMt.cient es

    public class XXService private RestTemplate restTemplate public XXService RestTemplate restTemplate ObjectMapper objectM
  • 【Vue基础系列】Vue中的过滤器(filter)

    一 Vue中的过滤器是什么 过滤器 filter 是输送介质管道上不可缺少的一种装置 大白话 就是把一些不必要的东西过滤掉 过滤器实质不改变原始数据 只是对数据进行加工处理后返回过滤后的数据再进行调用处理 我们也可以理解其为一个纯函数 定义
  • linux下重启X

    linux下的重启X方法收集 主要是ubuntu下的的 不知道有没效果啊 一个个试 Ctrl Alt Backspace Alt Print Screen K 这几个键太难按了 不知道ubuntu后来为什么这么设计 sudo restart
  • Windows10更新后麦克风无法使用(无法找到输入设备)解决方案

    1 问题描述 Windows10某次更新以后 无法使用麦克风在视频会议中讲话 在声音设置中 显示无法找到输入设备 在设备管理中 音频输入和输出栏 也未显示有麦克风 2 问题原因 无法找到声音的输出设备很可能是驱动损坏而导致的问题 一般是系统
  • 什么是 RESTful ?到底 REST 和 SOAP、RPC 有何区别?

    什么是 RESTful 到底 REST 和 SOAP RPC 有何区别
  • 51单片机编译警告笔记一(WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS)

    1 警告示例 WARNING L16 UNCALLED SEGMENT IGNORED FOR OVERLAY PROCESS SEGMENT PR DELAY N10US DELAY WARNING L16 UNCALLED SEGMEN
  • 使用C/C++编写GDALWarp的方法

    使用C C 编写GDALWarp的方法 GDAL Geospatial Data Abstraction Library 是一个开源的地理空间数据处理库 提供了许多功能强大的工具和函数 用于读取 写入和处理各种地理空间数据格式 其中的GDA
  • linux 查看java的安装路径

    获取java安装路径前要判断是否已经安装成功java 执行命令 java Usage java options class args to execute a class or java options jar jarfile args t
  • SpringBoot @ControllerAdvice异常处理

    ControllerAdvice需要与 ExceptionHandler配合使用 ControllerAdvice放在类上 表明这个类用以处理异常 ExceptionHandler XXException class 则用在具体的方法上 表
  • CSAPP chap10

    chap10 系统级I O 一 老师一直说让我们使用命令ls l看能得到什么 今天终于去看了 drwxrwxrwx 第一位是表示文件类型 d是目录文件 l是链接文件 是普通文件 第2 4位是表示这个文件的自己拥有的权限 r是读 w是写 x是
  • C语言 运算符与表达式

    目录 前言 赋值运算符 赋值表达式 1 赋值运算符 2 赋值表达式 3 赋值语句 4 用法 强制类型转换符 1 自动转换 2 强制转换 算术运算符 算术表达式 1 算术运算符 2 算术表达式 自增自减运算符 sizeof运算符 复合赋值运算
  • FasterNet:CVPR2023年最新的网络,基于部分卷积PConv,性能远超MobileNet,MobileVit

    文章目录 摘要 1 简介 2 相关工作 3 PConv和fastnet的设计 3 1 准备工作 3 2 部分卷积作为基本算子 3 4 FasterNet作为一般的主干网络 4 实验结果 4 1 PConv是快速的 但具有很高的FLOPS 4
  • 软路由的负载均衡设置:优化网络性能和带宽利用率

    在现代网络环境中 提升网络性能和最大化带宽利用率至关重要 通过合理配置软路由IP的负载均衡设置 可以有效地实现这一目标 并提高整体稳定性与效果 本文将详细介绍如何进行软路由IP的负载均衡设置 从而优化网络表现 增加带宽利用效率 并为读者呈现
  • (笔记)googletest在C语言项目中的使用指南

    文章目录 0 背景 需求说明 0 1 需求 0 2 方法 0 3 构建 1 gtest文件层级划分与说明 1 1 单元测试运行方法 2 新增单元测试 2 1 新增模块测试 2 2 新增模块功能测试 3 单元测试具体实现 3 1 gtest