【可测试性实践】C++单元测试:gtest gmock

引言

google test是目前C++主流的单元测试框架,本文介绍如何在工程引入gtest和gmock,并提供入门参考示例。根据黄金圈思维我们先思考Why(为什么做),为什么我们要进行单元测试,为什么要引入mock手段来测试代码,然后再来思考How(怎么做),最后思考What(取得了什么效果)。

Why:为什么引入单元测试?

我们先来看测试金字塔,如下图所示:

图引自:https://anymindgroup.com/news/tech-blog/15053/

可以看到从下往上分别是:

  1. Unit tests:单元测试
  2. Service/Integration tests:集成测试、端到端测试
  3. User Interface tests:用户界面测试

越接近代码的测试,速度也就越快,成本也就越低。单元测试就最贴近代码的,在开发过程中执行测试就越容易发现问题。另外覆盖率是测试金字塔的核心,越接近底层的测试覆盖率应该越高,所以我们通常会以代码覆盖率增量代码覆盖率来佐证单元测试的效果。

我们引入单元测试有两个目标:

  • 提升测试速度和降低测试成本
  • 提升代码可测试性

但最终的目的只有一个:提升质量

再来说说为什么引入mock:

  • 解决环境依赖的问题(网络、数据库等)
  • 更早的实现接口逻辑(在后端服务未准备好之前),减少等待
  • 通过模拟真实对象更好的驱动进行代码设计

当然Why层面可以做更多的深入思考,这里主要是抛砖引玉。

How:如何使用gtest & gmock?

示例工程:UnitTestProj

src/hello_test.cpp添加以下代码

#include <gtest/gtest.h>TEST(HelloTest, BasicAssertions) {// Expect two strings not to be equal.EXPECT_STRNE("hello", "world");// Expect equality.EXPECT_EQ(7 * 6, 42);
}

代码解析:

  • TEST 宏定义了一个测试用例。
  • HelloTest 是测试套件的名称,可以包含多个测试用例。
  • BasicAssertions 是测试用例的名称,用于描述具体的测试内容。
  • EXPECT_STRNE("hello", "world") 断言两个字符串不相等。
  • EXPECT_EQ(7 * 6, 42) 断言两个数值相等。

src/test_mock.cpp添加以下代码

#include <gtest/gtest.h>
#include <gmock/gmock.h>// 定义一个接口
class MyInterface {
public:virtual ~MyInterface() = default;virtual int Foo(int x) = 0;
};// 使用gmock生成Mock类
class MockMyInterface : public MyInterface {
public:MOCK_METHOD(int, Foo, (int x), (override));
};TEST(MockTestSuite, MockTestCase) {MockMyInterface mock;EXPECT_CALL(mock, Foo(5)).Times(1).WillOnce(testing::Return(10));ASSERT_EQ(mock.Foo(5), 10);
}

代码解析:

  • 定义了一个纯虚接口 MyInterface,其中包含一个纯虚函数 Foo,需要在派生类中实现。
  • 使用 Google Mock 提供的 MOCK_METHOD 宏生成 MockMyInterface 类,该类继承自 MyInterface 并实现了 Foo 函数。MOCK_METHOD 宏的参数包括返回类型、函数名、参数列表和覆盖说明符(override)。
  • TEST(MockTestSuite, MockTestCase) 定义了一个测试用例,属于测试套件 MockTestSuite
  • 使用 EXPECT_CALL 宏设置期望的函数调用。在这里,期望 mock.Foo(5) 被调用一次,并返回 10
  • 使用 ASSERT_EQ 宏断言 mock.Foo(5) 的返回值是否等于 10

CMake配置示例

cmake_minimum_required(VERSION 3.14)
project(UnitTestProj)# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)include(FetchContent)
FetchContent_Declare(googletestURL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)# 启用测试
enable_testing()# 添加头文件目录
include_directories(${CMAKE_SOURCE_DIR}/src)# 链接Google Test和Google Mock库
include_directories(${gtest_SOURCE_DIR}/include ${gmock_SOURCE_DIR}/include)# 添加测试源文件
add_executable(unit_test_demosrc/hello_test.cpp src/test_mock.cpp
)# 链接Google Test、Google Mock
target_link_libraries(unit_test_demo gtest gmock gtest_main)# 包含Google Test的发现测试功能
include(GoogleTest)
# 使用gtest_discover_tests命令自动发现并运行unit_test_demo目标中的所有测试
gtest_discover_tests(unit_test_demo)

运行测试结果

What:单元测试带来什么好处?

当然从这个简单的demo是很难体现单元测试带来的好处的,这里需要用实际的项目数据来进行佐证,但这往往是最难的部分,并且具有一定的滞后性,因为单测短期内对研发一定会带来成本的提升,需要自上而下认可并愿意投入资源去提升。

这里提供一些业界的参考指标,在我们引入单元测试后,尝试使用如下指标来衡量成果:

  1. 代码覆盖率:使用工具(如 gcov、JaCoCo)生成覆盖率报告,目标是达到 80% 以上的行覆盖率和分支覆盖率。
  2. 缺陷检测率:统计单元测试发现的缺陷数量和上线后发现的缺陷数量,目标是单元测试发现 70% 以上的缺陷。
  3. 测试执行时间:确保单元测试套件在 5 分钟内完成执行,以便快速反馈。
  4. 测试通过率:目标是测试通过率达到 95% 以上,确保代码的稳定性。
  5. 测试维护成本:定期评估测试用例的维护成本,确保在代码变更时需要较少的修改。
  6. 覆盖的功能模块:确保所有关键功能模块都有对应的单元测试覆盖。

更多参考

ASSERT_ vs EXPECT_

关于gtest有两种类型的断言,我们在使用的时候可以参考以下对比:

特性ASSERT_ 系列断言EXPECT_ 系列断言
行为断言失败时立即终止当前测试用例断言失败时继续执行当前测试用例
适用场景后续代码依赖于当前断言的结果希望即使断言失败,后续代码仍然执行
示例ASSERT_EQ(a, b);EXPECT_EQ(a, b);

gmock 使用指南

Google Mock(gmock)是 Google Test 的一个扩展库,专门用于创建和使用模拟对象。在进行单元测试时,模拟对象可以用来替代真实对象,从而隔离待测代码和依赖的外部组件。

基本概念

  1. 模拟类(Mock Class):一个类的模拟实现,使用宏定义来替代实际方法的实现。
  2. 期望(Expectations):定义模拟对象的预期行为,比如函数调用的次数和返回值。
  3. 行为(Actions):指定模拟对象在满足期望时应该执行的操作,比如返回特定值或调用真实方法。

常用功能

1. 设置调用次数
  • Times(n):期望函数被调用 n 次。
  • Times(testing::AtLeast(n)):期望函数被调用至少 n 次。
  • Times(testing::AtMost(n)):期望函数被调用至多 n 次。
2. 设置返回值
  • WillOnce(testing::Return(value)):指定函数一次调用返回 value
  • WillRepeatedly(testing::Return(value)):指定函数多次调用返回 value
3. 参数匹配器
  • testing::Eq(val):匹配等于 val 的参数。
  • testing::Ne(val):匹配不等于 val 的参数。
  • testing::Lt(val):匹配小于 val 的参数。
  • testing::Le(val):匹配小于等于 val 的参数。
  • testing::Gt(val):匹配大于 val 的参数。
  • testing::Ge(val):匹配大于等于 val 的参数。
  • testing::StrEq(str):匹配等于 str 的字符串参数。
4. 动作(Actions)
  • WillOnce(testing::Return(value)):指定函数一次调用返回 value
  • WillOnce(testing::Invoke(func)):指定函数一次调用执行 func
  • WillRepeatedly(testing::Return(value)):指定函数多次调用返回 value
  • WillRepeatedly(testing::Invoke(func)):指定函数多次调用执行 func

写在最后

本文通过黄金思维圈来思考引入单元测试带来的价值,并基于C++工程来接入gtest和gmock来完成基础单测和mock场景的使用。TDD的理念已经存在很久了,相信在互联网行业多多少少都听过,但为什么国内依然很少有团队能做好,这里面有很多原因,比如:

  • 写单测的ORI(投入产出比)问题,需要自上而下认可并愿意投入资源
  • 写单测需要程序员额外投入时间,并不算做KPI的业绩
  • 互联网产品迭代变化快,维护单测成本高
  • 国内程序员工程素养参差不齐,单测普及率不高或不知道怎么写
  • 存量代码不好测,改造成本较高

所以本文并不是为了鼓吹单测有多好,而是提供一种提升代码质量的思路,TDD更像是一种理念,工具是用来提升效率的,需要持续打磨才能发挥效用。笔者在工程引入gtest的过程中也同样遇到不少问题,后续有机会继续分享实际的案例和效果。

附录

  • https://google.github.io/googletest/
  • https://support.huaweicloud.com/bestpractice-testman/cloudtest_14_0004.html
  • https://testerhome.com/topics/33679
  • https://www.cnblogs.com/edisonchou/p/talk_about_what_is_testing_pyramids.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/143310.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

IBM中国研发部裁员:全球化背景下的中国IT产业挑战与机遇

文章目录 每日一句正能量前言整体分析人才发展裁员对中国IT人才市场的影响&#xff1a;IT从业者提升竞争力的策略&#xff1a; 产业未来后记 每日一句正能量 一切美好的事物都是曲折地接近自己的目标&#xff0c;一切笔直都是骗人的&#xff0c;所有真理都是弯曲的&#xff0c;…

设计一个高质量的API接口:提升应用性能的关键步骤

在当今的软件开发世界中&#xff0c;API&#xff08;应用程序编程接口&#xff09;接口扮演着至关重要的角色。一个设计精良的API不仅能够提高开发效率&#xff0c;还能提升用户体验&#xff0c;并确保系统的可扩展性和安全性。本文将探讨如何设计一个高质量的API接口&#xff…

蓝卓标杆客户镇洋发展,荣获IDC中国未来企业大奖

9月11-12日&#xff0c;2024 IDC中国年度盛典暨颁奖典礼在上海正式举办&#xff0c;年度盛典公布了2024 IDC中国未来企业大奖等系列奖项&#xff0c;以此表彰不同机构/组织/个人在数字化转型大背景下的努力与成绩。 其中&#xff0c;浙江镇洋发展股份有限公司&#xff08;以下简…

【sgCreateCallAPIFunction】自定义小工具:敏捷开发→调用接口方法代码生成工具

<template><div :class"$options.name" class"sgDevTool"><sgHead /><div class"sg-container"><div class"sg-start"><div style"margin-bottom: 10px">调用接口方法定义列表</div…

Gradio导入AIGC大模型创建web端智能体聊天机器人,python(2)

Gradio导入AIGC大模型创建web端智能体聊天机器人&#xff0c;python&#xff08;2&#xff09; 选用这个大模型&#xff1a; https://huggingface.co/HuggingFaceTB/SmolLM-1.7B-Instructhttps://huggingface.co/HuggingFaceTB/SmolLM-1.7B-Instruct原因是该模型相对比较小&am…

使用Dev-C++实现比大小的C语言程序

使用Dev-C++实现比大小的C语言程序 引言一、Dev-C++开发环境简介与安装1.1 Dev-C++简介1.2 Dev-C++安装步骤二、初识C语言与Dev-C++的使用2.1 C语言基础2.2 创建一个新的C项目2.3 编写简单的C程序2.4 编译与运行三、编写比大小的C程序3.1 程序需求3.2 程序设计3.3 编写代码3.4 …

dubbo三

dubbo dubbo架构各层说明 URL举例解析 消费者引用服务过程 项目初始化

.ideavimrc在idea打不开

### bug修复 自己不知道搞了啥导致.ideavimrc文件打不开&#xff0c;如图点击无反应 ![[Pasted image 20240917004710.png|400]] 后面发现是格式类型的错误 参考[这篇文章](https://blog.csdn.net/qq_41147260/article/details/85002668) **修复** - AltCtrls 打开设置 - Edi…

win11:重命名文件自动改变位置

你用的系统有个小bug 就是在桌面上重命名文件之后 文件会自动跳转到左上角 有些不注意的以为文件丢了 分类摆放好的文件 重命名之后还得拉回原来的位置 其实简单设置一下 就可以修复这个小bug 1、我们找到设置 在搜索栏搜索“个性化”&#xff0c;选择---‘开始’菜单设置 2、…

【北京迅为】《STM32MP157开发板使用手册》- 第三十一章Cortex-M4窗口看门狗实验

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

java进销存系统源码:管店云进销存解决方案

在当今数字化转型的大背景下&#xff0c;企业对高效、可靠的进销存管理系统的需求日益增长。Java作为一种广泛使用的编程语言&#xff0c;以其成熟的技术栈和强大的生态系统&#xff0c;成为了开发高性能进销存系统的首选语言之一。本文将介绍一款基于Java进销存系统源码的“管…

yolov5s网络结构

鉴于网上关于yolov5s的解读众多然不尽相同&#xff0c;特从YOLOv5官方项目地址&#xff1a;https://github.com/ultralytics/yolov5下载了yolov5的各版本文件并上传至 [资源] 栏目&#xff0c;这里就yolov5s版本的网络结构进行分析展示。 1、yolov5s模型的yaml文件 # Ultralyt…

Git使用教程-将idea本地文件配置到gitte上的保姆级别步骤

&#x1f939;‍♀️潜意识起点&#xff1a;个人主页 &#x1f399;座右铭&#xff1a;得之坦然&#xff0c;失之淡然。 &#x1f48e;擅长领域&#xff1a;前端 是的&#xff0c;我需要您的&#xff1a; &#x1f9e1;点赞❤️关注&#x1f499;收藏&#x1f49b; 是我持…

基于LangChain的Embedding开发手册(保姆级)

前言 时至今日&#xff0c;经过2年的“攻城拔寨”&#xff0c;大模型显然吹进了“寻常百姓家”。如果你还不了解ChatGPT&#xff0c;不了解通义、文心、混元等国内任意一款大模型产品&#xff0c;那么请来博主这里坐坐&#xff0c;我们“边看边聊”。 随着ChatGPT的问世&#…

Stable Diffusion绘画 | ControlNet应用-IP-Adapter:堪比 Midjourney 垫图

IP-Adapter 是腾讯AI实验室研发的控制器&#xff0c;属于 ControlNet 最强控制器前三之一。 如果想参照图片的风格&#xff0c;生成各种各样类似效果的图片&#xff0c;就可以用到 IP-Adapter。 在 ControlNet 单元中上传一张图片&#xff1a; 不输入任何提示词&#xff0c;出图…

Python3网络爬虫开发实战(17)爬虫的管理和部署(第一版)

文章目录 一、 Scrapyd 分布式部署1.1 了解 Scrapyd1.2 准备工作1.3 访问 Scrapyd1.4 Scrapyd 的功能1.5 ScrapydAPI 的使用 二、Scrapyd-Client 的使用2.1 准备工作2.2 Scrapyd-Client 的功能2.3 Scrapyd-Client 部署 三、Scrapyd 对接 Docker3.1 准备工作3.2 对接 Docker 四、…

Flask 设置session 自定义登录验证

"""1. 设置session# 设置session成功 重定向到首页session.permanent True # 设置会话过期时间session[info] usernamereturn redirect(url_for(index))2. 获取sessioninfo session.get(info, default0)return render_template(index.html, infoinfo)3. 设置…

Java笔试面试题AI答之单元测试JUnit(6)

文章目录 31. 如果将JUnit测试方法声明为“私有”会发生什么&#xff1f;1. 测试方法不会被JUnit执行2. 违反JUnit的设计原则3. 潜在的测试覆盖率问题4. 解决方案 32. 如果声明JUnit测试方法返回“String”会发生什么&#xff1f;编译错误运行时行为正确的做法结论 33. 可以使用…

变电站缺陷隐患检测图像数据集,总共包含8000张图片,包含渗漏油,鸟巢,表盘破损,呼吸器变色等

变电站缺陷隐患检测图像数据集&#xff0c;总共包含8000张图片&#xff0c;包含渗漏油&#xff0c;鸟巢&#xff0c;表盘破损&#xff0c;呼吸器变色等。 变电站缺陷隐患检测图像数据集营销介绍 数据集名称 变电站缺陷隐患检测图像数据集&#xff08;Substation Defect Detect…

微分电路设计

1 简介 微分电路会根据电路时间常数和放大器的带宽来输出某个频率范围内输入信号的微分。向反相输入施加输入信号&#xff0c;输出相对输入信号的极性会反相。理想的微分器电路基本都不稳定&#xff0c;需要增加输入电阻器和反馈电容器&#xff0c;才能达到稳定&#xff0c;但是…