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

news2024/11/15 7:35:18

引言

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(
  googletest
  URL 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_demo
  src/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.coloradmin.cn/o/2152946.html

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

相关文章

Linux:路径末尾加/和不加/的区别

相关阅读 Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm1001.2014.3001.5482 普通文件操作 首先说明这个问题只会出现在目录和符号链接中&#xff0c;因为如果想要索引普通文件但却在路径末尾加/则会出现错误&#xff0c;如例1所示。 # 例1 zhang…

Django SQL注入-漏洞分析

1.进入项目界面 图1 项目主界面 2.访问任意不存在的目录路径报错&#xff0c;提示存在demo接口 图2 提示存在接口 3.访问/demo/&#xff0c;提示有一个name参数 图3 发现隐藏参数 4.对接口参数进行fuzz&#xff08;实战思路&#xff09;&#xff0c;vulfocus已经给出了/demo?…

Innodb存储架构

Innodb整体存储架构 Innodb是一款兼顾性能及可靠性的存储引擎&#xff0c;主要分为内存存储结构和磁盘存储结构&#xff0c;二者分别扮演着提高性能和数据持久化的工作 内存结构中定义了缓冲池、变更缓冲区、日志缓冲区、自适应哈希四个缓冲区&#xff0c;它们均是为提升查询…

docker技术(上)

一、docker简介 Docker 是一个开源的应用容器引擎&#xff0c;于 2013 年由 Solomon Hykes 推出并开源。它基于 Go 语言开发&#xff0c;遵从 Apache2.0 协议。Docker 可以让开发者将应用及其依赖包打包到一个可移植的容器中&#xff0c;然后发布到任何流行的 Linux 或 Windows…

文件外发控制怎么做?公司文件外发管控的方法(这五种方法你一定要学会!)

还在担心重要文件发出去就"人间蒸发"&#xff1f; 或者每次发送公司机密都提心吊胆&#xff1f; 其实&#xff0c;文件外发就像放风筝&#xff0c;你需要时刻握住“线头”&#xff0c;确保它不会飞得太远&#xff01; 今天我们来揭秘五种公司文件外发的神级管控方法…

基于SpringBoot的医院挂号就诊系统【附源码】

基于SpringBoot的高校社团管理系统&#xff08;源码L文说明文档&#xff09; 目录 4 系统设计 4.1界面设计原则 4.2功能结构设计 4.3.2 数据库物理设计 第5章 系统实现 5.1用户信息管理 5.2 医生信息管理 5.3公告类型管理 5.1公告信息管理 4…

C++进阶(2):多态

多态的概念 多态分为编译时多态(静态多态)和运行时多态(动态多态)。**编译时多态&#xff1a;**主要就是我们前面讲的函数重载和函数模版。之所以叫编译时多态&#xff0c;是因为实参传给形参的参数匹配是发生在编译时完成的&#xff08;ps&#xff1a;通常把编译时一般归为静…

常见项目场景题1(数据量很大时如何去重,实现超时处理)

数据很多&#xff0c;限制内存&#xff0c;如何去重 对于大数据量去重的场景&#xff0c;我们可以考虑使用位图(Bitmap) Bitmap 是使用二进制来表示某个元素是否存在的数组。用0和1来表示存在与不存在 使用Bitmap的话&#xff0c;一个数字占用1bit&#xff0c;大大减少内存消耗…

JVM 调优篇8 调优案例5- 逃逸分析

一 逃逸分析 1.1 概念 逃逸分析的基本行为就是分析对象动态作用域&#xff1a;当一个对象在方法中被定义后&#xff0c;对象只在方法内部使用&#xff0c;则认为没有发生逃逸。当一个对象在方法中被定义后&#xff0c;它被外部方法所引用&#xff0c;则认为发生逃逸。例如作为…

打造未来企业:业务能力建模的实践应用与数字化转型的落地策略

在当今数字化迅速发展的时代&#xff0c;企业的转型迫在眉睫。通过数字技术提升运营效率、增强客户体验、优化资源配置成为了企业竞争的核心战略。《业务能力指南》为企业提供了清晰的业务能力建模框架&#xff0c;并指导企业如何将其应用于实际操作中&#xff0c;帮助企业在数…

(三)代码实现:Boustrophedon Cellular Decomposition Path Planning用珊格地图生成每个cell的覆盖路径

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录前言算法原理方法一&#xff1a;全地图进行牛耕覆盖步骤方法二&#xff1a;区域分解地图进行牛耕覆盖步骤凸多边形基于栅格地图的…

Windows系统文件夹中的文件名排序

一天张三、李四的同事周五接到王哥的一个任务需求&#xff0c;有一个文件夹&#xff0c;里面有许多图片文件&#xff0c;网页访问某个分类展示文件的时候&#xff0c;王哥希望文件名的展示顺序可以按照Windows资源管理器中文件名升序排序的方式展示。 网站图片目录中有如下图片…

程序遇到问题错误bug时的13种解决方法途径总结以及之前的一些具体例子

目录 1 信心--没有解决不了的bug 2 耐心、不要着急、静下心来、用脑思考 2.1 开始解决问题前不要着急&#xff0c;先思考 2.2 在解决问题的过程中也不要着急&#xff0c;要冷静思考 3 网络搜索 4 大模型问答&#xff1a;必应、kimi、通义千问、文心一言 5 看芯片手册、S…

0921VGG网络实现

深度学习之VGG网络搭建 1.VGG动机2.VGG架构3.代码4.结论1.VGG动机 随着卷积网络在计算机视觉领域的快速发展,越来越多的研究人员开始通过改变模型的网络结构在提高在图像识别任务中的精度,例如使用更小的卷积核和步长[2]。基于类似的想法,论文作者提出可以尝试通过改变卷积…

【设计模式】创建型模式(三):单例模式

创建型模式&#xff08;三&#xff09;&#xff1a;单例模式 1.概念2.案例3.实现方式3.1 懒汉式&#xff0c;线程不安全3.2 懒汉式&#xff0c;线程安全3.3 饿汉式3.4 双检锁/双重校验锁&#xff08;DCL&#xff0c;Double-Checked Locking&#xff09;3.5 登记式/静态内部类3.…

俄罗斯OZON新生儿产品好不好卖,OZON新生儿产品

Top1 遥控水球坦克 Танк на радиоуправлении стреляющий орбизами PANAWEALTH 商品id&#xff1a;1384249985 月销量&#xff1a;692 欢迎各位OZON卖家朋友点击这里选品&#xff1a; &#x1f449; D。DDqbt。COm/74rD 遥控射击水…

【项目管理进阶】风险问题

前言 各位盆友&#xff0c;你们期待的项目管理进阶系列有新的消息&#xff0c;请注意查收&#xff0c;并反馈哦~ 在参加项目的过程中&#xff0c;你是否面临或参加过类似如下的场面&#xff1a; 为了立项&#xff0c;先调研市场、技术、社会、组织内部的现状为了科学的管理项目…

如何使用Claude进行Android App开发 —— 基于Jetpack和Compose的电影App实例

如何使用Claude进行Android App开发 —— 基于Jetpack和Compose的电影App实例 近年来&#xff0c;人工智能&#xff08;AI&#xff09;在软件开发中的应用越来越广泛&#xff0c;帮助开发者在设计、编码、测试和优化中提高生产效率。Claude是Anthropic开发的一款强大的AI助手&…

Redis的三种持久化方法详解

Redis持久化机制详解 | JavaGuide Redis 不同于 Memcached 的很重要一点就是&#xff0c;Redis 支持持久化&#xff0c;而且支持 3 种持久化方式: 快照&#xff08;snapshotting&#xff0c;RDB&#xff09;只追加文件&#xff08;append-only file, AOF&#xff09;RDB 和 A…

Vue使用axios二次封装、解决跨域问题

1、什么是 axios 在实际开发过程中&#xff0c;浏览器通常需要和服务器端进行数据交互。而 Vue.js 并未提供与服务器端通信的接口。从 Vue.js 2.0 版本之后&#xff0c;官方推荐使用 axios 来实现 Ajax 请求。axios 是一个基于 promise 的 HTTP 客户端。 关于 promise 的详细介…