手把手教你使用gtest写单元测试

news2024/11/25 7:02:19

开源框架:gtest,它主要用于写单元测试,检查真自己的程序是否符合预期行为。这不是QA(测试工程师)才学的,也是每个优秀后端开发codoer的必备技能。

本期博文内容及使用的demo,参考:

  • Googletest Basic Guide[1]

  • Googletest Samples [2]

构建依赖环境

按照惯例,先介绍下怎么基于CMakeLists.txt构建依赖环境。

由于Google没有为googletest/samples中的samples写CMakeLists.txt,因此,gtest从github克隆下来后,也无法直接运行这些samples。

为方便大家跟着本文一起实践,获得更好的学习体验,在后台回复「gtest」即可获取我配置好的gtest压缩包。

当前目录结构如下:

$ tree -L 2
demo
├── CMakeLists.txt  
├── build        # 空的文件夹
├── include
│   ├── CMakeLists.txt
│   ├── gflags
│   └── googletest
├── main.cc
└── my_gtest_demo_1.cc

然后,在demo/build路径下,执行命令:

$ cmake .. && make -j 4

这些samples生成的可执行文件都在demo/build/bin路径下。

这样,就介绍完前提准备,下面开始进入正题。

assertion

在gtest中,是通过断言(assertion)来判断代码实现的功能是否符合预期。断言的结果分为success、non-fatal failture和fatal failture。

根据断言失败的种类,gtest提供了两种断言函数:

  • success:即断言成功,程序的行为符合预期,程序继续向下允许。

  • non-fatal failure:即断言失败,但是程序没有直接crash,而是继续向下运行。 gtest提供了宏函数EXPECT_XXX(expected, actual):如果condition(expected, actual)返回false,则EXPECT_XXX产生的就是non-fatal failure错误,并显示相关错误。

  • fatal failure:断言失败,程序直接crash,后续的测试案例不会被运行。 gtest提供了宏函数ASSERT_XXX(expected, actual)。 在写单元测试时,更加倾向于使用EXPECT_XXX,因为ASSERT_XXX是直接crash退出的,可能会导致一些内存、文件资源没有释放,因此可能会引入一些bug。

具体的EXPECT_XXX、ASSERT_XXX函数及其判断条件,如下两个表。

表1 一元比较

ASSERT

EXPECT

Verifies

ASSERT_TRUE(condition);

EXPECT_TRUE(condition);

condition is true

ASSERT_FALSE(condition)

EXPECT_FALSE(condition)

condition is false

表2 二元比较

ASSERT

EXPECT

Condition

ASSERT_EQ(val1, val2);

EXPECT_EQ(val1, val2);

val1 == val2

ASSERT_NE(val1, val2);

EXPECT_NE(val1, val2);

val1 != val2

ASSERT_LT(val1, val2);

EXPECT_LT(val1, val2);

val1 < val2

ASSERT_LE(val1, val2);

EXPECT_LE(val1, val2);

val1 <= val2

ASSERT_GT(val1, val2);

EXPECT_GT(val1, val2);

val1 > val2

ASSERT_GE(val1, val2);

EXPECT_GE(val1, val2);

val1 >= val2

Quick Start

下面以EXPECT_XXX为例子,快速开始使用gtest吧。

对于EXPECT_XXX,无论条件是否满足,都会继续向下运行,但是如果条件不满足,在报错的地方会显示:

  1. 没有通过的那个EXPECT_XXX函数位置;

  2. EXPECT_XXX第一个参数的值,即期待值

  3. EXPECT_XXX第二个参数的值,即实际值

如下demo:

// in gtest_demo_1.cc
#include <gtest/gtest.h>

int add(int lhs, int rhs) { return lhs + rhs; }

int main(int argc, char const *argv[]) {

    EXPECT_EQ(add(1,1), 2); // PASS
    EXPECT_EQ(add(1,1), 1) << "FAILED: EXPECT: 2, but given 1";; // FAILDED
    
    return 0;
}

编译执行后输出如下:

$ ./gtest_demo_1
/Users/self_study/Cpp/OpenSource/demo/gtest_demo_1.cc:9: Failure
Expected equality of these values:
  add(1,1)
    Which is: 2                # 期待的值
  1                            # 给定的值
FAILED: EXPECT: 2, but given 1 # 自己添加的提示信息 

可能你注意到了,在EXPECT_EQ(add(1,1), 1)后有个<<,这是因为gtest允许添加自定义的描述信息,当这个语句测试未通过时就会显示,比如上面的"FAILED: EXPECT: 2, but given 1"。

这个<<和std::ostream接受的类型一致,即可以接受std::ostream可以接受的类型。

相关视频推荐

程序员精进之路-从googletest测试框架开始

c++后端必学:googletest中的设计模式

c/c++后端开发需要学些什么?迭代13次的c/c++后端开发学习路线分享

需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

TEST

下面以googletest/samples中的sample1_unittest.cc中的demo为例,介绍如何更好地组织测试案例。

一个简单计算阶乘函数Factorial实现如下:

int Factorial(int n) {
  int result = 1;
  for (int i = 1; i <= n; i++) {
    result *= i;
  }

  return result;
}

怎么使用gtest来测试这个函数的行为?

按照上面的quick start可知,这个时候就可以使用EXPECT_EQ宏来判断:

 EXPECT_EQ(1, Factorial(-5)); // 测试计算负数的阶乘
  EXPECT_EQ(1, Factorial(0));   // 测试计算0的阶乘
  EXPECT_EQ(6, Factorial(3));   // 测试计算正数的阶乘 

但是当测试案例规模变大,不好组织。

因此,为了更好的组织test cases,比如针对Factorial函数,输入是负数的cases为一组,输入是0的case为一组,正数cases为一组。gtest提供了一个宏TEST(TestSuiteName, TestName),用于组织不同场景的cases,这个功能在gtest中称为test suite。

用法如下:

// 下面三个 TEST 都是属于同一个 test suite,即 FactorialTest

问题来了,怎么运行这些TEST?

在sample1_unittest.cc的main函数中,添加RUN_ALL_TESTS函数即可。

int main(int argc, char **argv) {
  printf("Running main() from %s\n", __FILE__);
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS(); 
}

在build/bin路径下,执行对应的可执行文件,输出如下:

$./sample1_unittest 
Running main() from /Users/self_study/Cpp/OpenSource/demo/include/googletest/googletest/samples/sample1_unittest.cc
[==========] Running 6 tests from 2 test suites. # 在 sample1_unittest.cc 中有两个 test suites
[----------] Global test environment set-up.    

# 第一个 test suite,即上面的 FactorialTest
[----------] 3 tests from FactorialTest     # 3 组
[ RUN      ] FactorialTest.Negative         # Negative 组输出
[       OK ] FactorialTest.Negative (0 ms)  # OK 表示 Negative 组全部测试通过
[ RUN      ] FactorialTest.Zero             # Zero组输出 
[       OK ] FactorialTest.Zero (0 ms)    
[ RUN      ] FactorialTest.Positive         # Positive组输出
[       OK ] FactorialTest.Positive (0 ms)   
[----------] 3 tests from FactorialTest (0 ms total)

# sample1_unitest 另一个测试案例的输出 ...

[----------] Global test environment tear-down  
[==========] 6 tests from 2 test suites ran. (0 ms total) 
[  PASSED  ] 6 tests.              # 全部测试结果:PASS表示全部通过 

下面稍微修改下sample1_unittest.cc中的代码,来产生一个错误:

TEST(FactorialTest, Negative) {
  EXPECT_EQ(10, Factorial(-5));  // 正确的应该是  EXPECT_EQ(1, Factorial(-5));
  // ...
}

重新编译,运行结果如下:

$ ./sample1_unittest 
Running main() from /Users/self_study/Cpp/OpenSource/demo/include/googletest/googletest/samples/sample1_unittest.cc
[==========] Running 6 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from FactorialTest
[ RUN      ] FactorialTest.Negative          # 开始运行上面修改的那个组
/Users/self_study/Cpp/OpenSource/demo/include/googletest/googletest/samples/sample1_unittest.cc:79: Failure                 # 测试失败,并指出错误case的位置
Expected equality of these values:           # 期待的值
  10
  Factorial(-5)                              # 实际计算出的值
    Which is: 1
[  FAILED  ] FactorialTest.Negative (0 ms)   # 这组case测试状态:FAILED
[ RUN      ] FactorialTest.Zero              # 下面继续运行
[       OK ] FactorialTest.Zero (0 ms)
[ RUN      ] FactorialTest.Positive
[       OK ] FactorialTest.Positive (0 ms)
[----------] 3 tests from FactorialTest (0 ms total)

# ...

[----------] Global test environment tear-down
[==========] 6 tests from 2 test suites ran. (0 ms total)
[  PASSED  ] 5 tests.          
[  FAILED  ] 1 test, listed below:     # 1个test失败
[  FAILED  ] FactorialTest.Negative    # 失败的test suite及其组

 1 FAILED TEST

此外,在TEST宏函数中,也可以像个普通函数一样,定义变量之类的行为。

比如在sample2_unittest.cc中,测试一个自定义类MyString的复制构造函数是否表现正常:

const char kHelloString[] = "Hello, world!";

// 在 TEST内部,定义变量
TEST(MyString, CopyConstructor) {
  const MyString s1(kHelloString);
  const MyString s2 = s1;
  EXPECT_EQ(0, strcmp(s2.c_string(), kHelloString));
}

为获得进一步学习,读者可以自行调整sample1_unittest.cc、sample2_unittest.cc中的TEST行为,加深对gtest的TEST宏的理解。

TEST_F

下面介绍gtest中更为高级的功能:test fixture,对应的宏函数是TEST_F(TestFixtureName, TestName)。

fixture,其语义是固定的设施,而test fixture在gtest中的作用就是为每个TEST都执行一些同样的操作。

比如,要测试一个队列Queue的各个接口功能是否正常,因此就需要向队列中添加元素。如果使用一个TEST函数测试Queue的一个接口,那么每次执行TEST时,都需要在TEST宏函数中定义一个Queue对象,并向该对象中添加元素,就很冗余、繁琐。

怎么避免这部分冗余的过程?

TEST_F就是完成这样的事情,它的第一个参数TestFixtureName是个类,需要继承testing::Test,同时根据需要实现以下两个虚函数:

  • virtual void SetUp():在TEST_F中测试案例之前运行;

  • virtual void TearDown():在TEST_F之后运行。

可以类比对象的构造函数和析构函数。这样,同一个TestFixtureName下的每个TEST_F都会先执行SetUp,最后执行TearDwom。

此外,testing::Test还提供了两个static函数:

  • static void SetUpTestSuite():在第一个TEST之前运行

  • static void TearDownTestSuite():在最后一个TEST之后运行

以sample3-inl中实现的class Queue为例:

class QueueTestSmpl3 : public testing::Test { // 继承了 testing::Test
protected:  
  
  static void SetUpTestSuite() {
    std::cout<<"run before first case..."<<std::endl;
  } 

  static void TearDownTestSuite() {
    std::cout<<"run after last case..."<<std::endl;
  }
  
  virtual void SetUp() override {
    std::cout<<"enter into SetUp()" <<std::endl;
    q1_.Enqueue(1);
    q2_.Enqueue(2);
    q2_.Enqueue(3);
  }

  virtual void TearDown() override {
    std::cout<<"exit from TearDown" <<std::endl;
  }
  
  static int Double(int n) {
    return 2*n;
  }
  
  void MapTester(const Queue<int> * q) {
    const Queue<int> * const new_q = q->Map(Double);

    ASSERT_EQ(q->Size(), new_q->Size());

    for (const QueueNode<int>*n1 = q->Head(), *n2 = new_q->Head();
         n1 != nullptr; n1 = n1->next(), n2 = n2->next()) {
      EXPECT_EQ(2 * n1->element(), n2->element());
    }

    delete new_q;
  }

  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

下面是sample3_unittest.cc中的TEST_F:

// in sample3_unittest.cc

// Tests the default c'tor.
TEST_F(QueueTestSmpl3, DefaultConstructor) {
  // !!! 在 TEST_F 中可以使用 QueueTestSmpl3 的成员变量、成员函数 
  EXPECT_EQ(0u, q0_.Size());
}

// Tests Dequeue().
TEST_F(QueueTestSmpl3, Dequeue) {
  int * n = q0_.Dequeue();
  EXPECT_TRUE(n == nullptr);

  n = q1_.Dequeue();
  ASSERT_TRUE(n != nullptr);
  EXPECT_EQ(1, *n);
  EXPECT_EQ(0u, q1_.Size());
  delete n;

  n = q2_.Dequeue();
  ASSERT_TRUE(n != nullptr);
  EXPECT_EQ(2, *n);
  EXPECT_EQ(1u, q2_.Size());
  delete n;
}

// Tests the Queue::Map() function.
TEST_F(QueueTestSmpl3, Map) {
  MapTester(&q0_);
  MapTester(&q1_);
  MapTester(&q2_);
}

以TEST_F(QueueTestSmpl3, DefaultConstructor)为例,再具体讲解下TEST_F的运行流程:

  1. gtest构造一个QueueTestSmpl3对象t1;

  2. t1.setUp初始化t1

  3. 第一个TEST_F即DefaultConstructor开始运行并结束

  4. t1.TearDwon运行,用于清理工作

  5. t1被析构

因此,sample3_unittest.cc输出如下:

% ./sample3_unittest
Running main() from /Users/self_study/Cpp/OpenSource/demo/include/googletest/googletest/samples/sample3_unittest.cc
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from QueueTestSmpl3
run before first case...    # 所有的test case 之前运行
[ RUN      ] QueueTestSmpl3.DefaultConstructor
enter into SetUp()          # 每次都会运行
exit from TearDown
[       OK ] QueueTestSmpl3.DefaultConstructor (0 ms)
[ RUN      ] QueueTestSmpl3.Dequeue
enter into SetUp()          # 每次都会运行
exit from TearDown
[       OK ] QueueTestSmpl3.Dequeue (0 ms)
[ RUN      ] QueueTestSmpl3.Map
enter into SetUp()          # 每次都会运行
exit from TearDown
[       OK ] QueueTestSmpl3.Map (0 ms)
run after last case...      # 所有test case结束之后运行
[----------] 3 tests from QueueTestSmpl3 (0 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 3 tests. 

TEST_F相比较TEST可以更加简洁地实现功能测试。

 

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

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

相关文章

40、Java 并发编程基础 ①

目录 一、进程&#xff08;Process&#xff09;二、线程&#xff08;Thread&#xff09;三、线程的串行四、多线程五、多线程原理六、多线程优缺点七、Java 的默认线程八、开启新线程(1) new Thread()(2) 继承 Thread&#xff0c;重写 run 方法(3) run() 和 start() 九、多线程…

AutoCV第八课:3D基础

目录 3D基础前言1. nuScenes数据集2. nuScenes数据格式3. 点云可视化总结 3D基础 前言 手写 AI 推出的全新保姆级从零手写自动驾驶 CV 课程&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考。 本次课程主要学习点云数据的可视化。 课程大纲可看下面的思维导图。…

【Shiro】SimpleAuthenticationInfo如何验证password

一、前言 通篇的关键就是知道ShiroRealm类重写的doGetAuthenticationInfo这个方法&#xff0c;到底是谁的方法。 从上图我们可以知道&#xff0c;ShiroRealm最终继承到了AuthenticatingRealm这个方法。 二、自定义的ShiroRealm类 ps&#xff1a;该图中①上的注释是没看过底…

Jetpack之livedata原理

1.LiveData是什么&#xff1f; 只有在生命周期处于started和resumed时。livedata才会更新观察者 2.Livedata的各种使用方式 1.更新数据 class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceSta…

c++之函数对象和谓词

目录 函数对象&#xff1a; 谓词&#xff1a; 一元谓词函数举例如下 二元谓词举例如下 函数对象和函数的区别 一元谓词的案例 二元函数对象案例 二元谓词案例 函数对象&#xff1a; 重载函数调用操作符的类&#xff0c;其对象常称为函数对象&#xff08;function obj…

内网渗透之linux到linux横向移动-ssh

0x01 一般情况下SSH密钥存放在~/.ssh/目录下&#xff0c;也可以文件中搜索已保存的SSH凭证 ~/.ssh/config ~/.ssh/known_hosts ~/.bash_history grep -ir "BEGIN RSA PRIVATE KEY" /* grep -ir "BEGIN DSA PRIVATE KEY" /* grep -ir "BEGIN OPENSSH…

SpringBoot入门学习笔记-快速认识

SpringBoot入门学习笔记-快速认识 快速案例入门案例解析parentstarter引导类内嵌tomcat ) 快速案例 在controller定义一个类 package com.ustc.sp5.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.…

redis 数据类型简介

redis 数据类型 redis的五种数据类型是&#xff1a;1、string&#xff08;字符串&#xff09;&#xff1b;2、hash&#xff08;哈希&#xff09;&#xff1b;3、list&#xff08;列表&#xff09;&#xff1b;4、set&#xff08;集合&#xff09;&#xff1b;5、sort set &…

QT初体验:手把手带你写一个自己的串口助手

前言 本文记录一下用QT Creator 写一个基本功能齐全的串口助手的过程&#xff0c;整个工程只有几百行代码&#xff0c;跟着做下来对新手来说可以更快了解整个QT项目的开发过程和一些常用控件的使用方法。对新手学习QT能增强信心&#xff0c;话不多说&#xff0c;正文开始 先看…

Mysql日志redo log、bin log、undo log 区别与作用及二阶段提交

一、redo log 重做日志 作用&#xff1a;确保事务的持久性。防止在发生故障的时间点&#xff0c;尚有脏页未写入磁盘&#xff0c;在重启mysql服务的时候&#xff0c;根据redo log进行重做&#xff0c;从而达到事务的持久性这一特性。 内容&#xff1a;物理格式的日志&#x…

46-Dockerfile-USER/WORKDIR指令

USER/WORKDIR指令 前言USER作用格式使用示例 WORKDIR作用格式说明使用示例 前言 本篇来学习下Dockerfile中的USER/WORKDIR指令 USER 作用 指定运行容器时的用户名或 UID&#xff0c;后续的RUN等指令也会使用指定的用户身份 说明&#xff1a; USER 只是帮助我们切换到指定的…

12_Uboot启动流程_4

目录 images全局变量 do_bootz函数 bootz_start函数 do_bootm_states函数 bootm_os_get_boot_func函数 do_bootm_linux函数 images全局变量 不管是bootz还是bootm命令,在启动Linux内核的时候都会用到一个重要的全局变量:images, images在文件cmd/bootm.c中有如下定义: i…

【2023/05/09】Scratch

Hello&#xff01;大家好&#xff0c;我是霜淮子&#xff0c;2023倒计时第4天。 Share The mighty desert is burning for the love of a blade of grass who shaks her head and laughs and flies away. 译文&#xff1a; 无垠的沙漠热烈追求一叶绿草的爱&#xff0c;她摇摇…

PCL中点云分割算法简析

文章目录 前言一、点云分割算法简介1.1 基于RANSAC的点云分割1.2 基于聚类的点云分割1.2.1 欧式聚类分割 1.3 基于深度学习的点云分割 二、算法示例2.1 基于RANSAC的平面分割2.2 欧式聚类2.3 基于PointNet的点云分割 总结 前言 点云分割算法广泛应用于激光遥感、无人驾驶、工业…

centos安装nginx教程

安装所需环境 Nginx 是 C语言 开发&#xff0c;建议在 Linux 上运行&#xff0c;当然&#xff0c;也可以安装 Windows 版本&#xff0c;本篇则使用 CentOS 7 作为安装环境。 一. gcc 安装 安装 nginx 需要先将官网下载的源码进行编译&#xff0c;编译依赖 gcc 环境&#xff0c…

【ESD专题】案例:TVS管钳位电压能不能通过TLP测试数据表征?

这几天遇到一个问题,就是还是想说TVS管导入的时候需要进行IEC61000-4-2 8kV接触静电的钳位波形测试。 比如有时可以看到规格书中给出对应的在IEC61000-4-2 8kV接触时的真实钳位波形: 根据我们文章【ESD专题】TVS管的选择的误区及钳位电压测试方法和一些参考手册所说…

计算机操作系统第四版第七章文件管理—课后习题答案

1.何谓数据项、记录和文件&#xff1f; 数据项&#xff1a;是最低级的数据组织形式&#xff0c;可以分为两种类型&#xff1a;基本数据项和组合数据项。基本数据项是用于描述一个对象的某种属性的字符集&#xff0c;是数据组织中可以命名的最小逻辑数据单位&#xff0c;又称为字…

使用volta对node版本进行控制

安装volta 首先下载volta 下载完成之后在电脑上使用命令行工具查看是否安装成功 volta -v 使用 volta -h 命令可以查看volta的一些用法 安装全局的node版本,可以有三种,第一种是安装最新的,第二种是安装某一个大版本下的,第三种是安装指定的node版本(安装的时候需要等待一段时…

Rest风格复习

Rest风格复习 简介三种注解的风格快速开发 简介 通过一段路径和访问方式来确定访问资源的行为方式 使用POST方式 // value定义 路径 method定义访问的方式RequestMapping(value "/users",method RequestMethod.POST)ResponseBodypublic String save(){System.out.…

Andrew Ng和OpenAI教你写prompt

课程地址&#xff1a; https://learn.deeplearning.ai/chatgpt-prompt-engb站搬运&#xff1a; https://www.bilibili.com/video/BV1No4y1t7Zn 教学人员&#xff1a;Lsa Fulford&#xff0c; Andrew NG LLM的两种样式 Base LLM:基于文本训练数据预测下一个词的概率&#xff0…