我平时的技术支持工作主要是快速阅读和调试代码,没有太多的代测试和验证需求,所以对单元测试一直没有留意。

直到我开始从头写一些密码学的基本功能代码,例如各种哈希算法,分组密码算法,流密码算法,分组密码的各种工作模式,各种国密算法的时候,才意识到我也需要去了解并使用测试。

尤其是有一次,我已经验证正确的某个代码,在此基础上实现新功能时,修改了底层代码,看起来好像没啥问题,然后在将来的某一天才发现那个修改引入了bug,又得重头查找问题的原因。如果有完整的测试,恐怕当场就能发现,而不是在很久后的某一天。

关于我对驱动测试的重新认识,也非常感谢 Leo Wang 曾经推荐的一篇关于测试的文章。

几年前学习Android Update Engine的时候曾经留意过其使用单元测试,所以这次学习单元测试框架时特别去看了那个框架,再次深入了解学习gtest (GoogleTest)框架。

由于平时使用的是 C,而 gtest 基于 C++,因此没法直接使用,需要一些额外的工作来对接 C 和 C++。

初学 gtest 时,特别参考了这篇:《嵌入式平台使用gtest进行白盒测试》,再次一并对作者表示感谢。

废话说了一大堆,下面直入主题。本文主要介绍如何快速将 gtest 引入到现有的 C 语言程序中进行测试。

0. 导读

第 1,2 节展示了一个完整的示例,将自己写的用于测试 gcd 和 factorial 函数的两个文件(gcd.c, factorial.c)和 googletest 整合到一起,然后运行测试,略显繁琐。

如果只希望了解整合的步骤,请直接跳转到第 3 节。

1. 原理

将 gtest 框架应用到 C 程序的原理比较简单,主要分 3 步:

  1. 编译测试框架 googletest 源码得到 libgtest.a 库文件(也可以根据需要编译成动态链接库或共享库文件.so)
  2. 编译待测试的 C 代码得到一个功能库文件,例如 libfoo.a
  3. 写一个单元测试文件(如:foo_unitttest.cc), 编译链接上 libgtest.a 和 libfoo.a 生成可执行文件进行测试

如果代码规模不大,对代码模块化要求也不高的话,可以将第2和第3步合并到一起执行,变成:

  1. 编译测试框架 googletest 源码得到 libgtest.a 库文件
  2. 写一个单元测试文件(如:foo_unitttest.cc),和待测试代码一起编译并链接到 libgtest.a 库得到可执行文件进行测试

2. 两步操作快速集成 gtest

2.1 准备 gtest 库文件和头文件

复制下面的代码分别保存为 CMakeLists.txt 和 gtest.mk 文件。

  1. CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(gtestlib)# GoogleTest requires at least C++11
set(CMAKE_CXX_STANDARD 11)include(FetchContent)
FetchContent_Declare(googletestURL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip
)# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
  1. gtest.mk
.PHONY: all cleanCMAKE   = cmake
MAKE    = makeGTEST             = .GTEST_BUILD_DIR   = $(GTEST)/build
GTEST_INSTALL_DIR = $(GTEST)/gtest
GTEST_CMAKELIST   = $(GTEST)/CMakeLists.txtGTEST_LIB         = $(GTEST_INSTALL_DIR)/lib/libgtest.a
GTEST_LIBMAIN     = $(GTEST_INSTALL_DIR)/lib/libgtest_main.aall:if [ ! -e $(GTEST_LIB) ] || [ ! -e $(GTEST_LIBMAIN) ]; then \mkdir -p $(GTEST_BUILD_DIR) && \$(CMAKE) -S $(GTEST) -B $(GTEST_BUILD_DIR) -DBUILD_GMOCK=OFF -DCMAKE_INSTALL_PREFIX=$(GTEST_INSTALL_DIR) && \$(MAKE) -C $(GTEST_BUILD_DIR) && \$(MAKE) -C $(GTEST_BUILD_DIR) install; \ficlean:if [ -e $(GTEST_BUILD_DIR) ]; then \$(MAKE) -C $(GTEST_BUILD_DIR) clean; \firm -rf $(GTEST_BUILD_DIR)rm -rf $(GTEST_INSTALL_DIR)

将上面的两个文件存放到同一个文件夹下,然后执行make -f gtest.mk

代码会自动下载 googletest 最新的 v1.11 版本进行编译,并将生成的库文件和头文件输出到当前文件夹的 gtest 目录下,如下:

$ tree gtest
gtest
├── include
│   └── gtest
│       ├── gtest-death-test.h
│       ├── gtest.h
│       ├── gtest-matchers.h
│       ├── gtest-message.h
│       ├── gtest-param-test.h
│       ├── gtest_pred_impl.h
│       ├── gtest-printers.h
│       ├── gtest_prod.h
│       ├── gtest-spi.h
│       ├── gtest-test-part.h
│       ├── gtest-typed-test.h
│       └── internal
│           ├── custom
│           │   ├── gtest.h
│           │   ├── gtest-port.h
│           │   ├── gtest-printers.h
│           │   └── README.md
│           ├── gtest-death-test-internal.h
│           ├── gtest-filepath.h
│           ├── gtest-internal.h
│           ├── gtest-param-util.h
│           ├── gtest-port-arch.h
│           ├── gtest-port.h
│           ├── gtest-string.h
│           └── gtest-type-util.h
└── lib├── cmake│   └── GTest│       ├── GTestConfig.cmake│       ├── GTestConfigVersion.cmake│       ├── GTestTargets.cmake│       └── GTestTargets-noconfig.cmake├── libgtest.a├── libgtest_main.a└── pkgconfig├── gtest_main.pc└── gtest.pc8 directories, 31 files
  • 目录 gtest/inlcude 包含 gtest 的头文件,写测试时需要包含头文件gtest/gtest.h
  • 目录 gtest/lib 包含 gtest 的库文件,链接时需要链接 libgtest.a 和 libgtest_main.a

特别说明:

  1. 由于是自动下载代码,所以需要 cmake 在 v3.14 以上;
  2. gtest生成的两个库文件中,只有 libgtest.a 是必须的,另外一个库文件 libgtest_main.a是 gtest 运行的入口,实际上就是一个 main 函数,如果你在自己的单元测试中定义了自己的 main 函数去调用 gtest,那就不需要 libgtest_main.a

2.2 编写测试代码

  • 待测试文件

这里的测试文件有两个, 分别是 gcd.c 和 factorial.c。

  1. foo.h, 头文件,用于函数声明
#ifndef __FOO_H__
#define __FOO_H__
#ifdef __cplusplus
extern "C"
{#endifint gcd(int a, int b);int factorial(int n);#ifdef __cplusplus
}
#endif
#endif
  1. gcd.c,用于计算两个数的最大公约数
#include "foo.h"int gcd(int a, int b)
{if (b == 0){return a;}else{return gcd(b, a % b);}
}
  1. factorial.c,用于计算一个数的阶乘
#include "foo.h"int factorial(int n)
{int i, res;res = 1;for (i=n; i>0; i--){res *= i;}return res;
}
  • 单元测试文件

写一个单元测试文件 foo_unittest.cc,包含两组测试,分别用于测试函数 gcd 和 factorial:
foo_unittest.cc

#include "foo.h"
#include <gtest/gtest.h>TEST(GCDTest, EvenTest)
{EXPECT_EQ(2, gcd(4, 10));EXPECT_EQ(6, gcd(30, 18));EXPECT_EQ(15, gcd(30, 45));
}TEST(GCDTest, PrimeTest)
{EXPECT_EQ(1, gcd(23, 10));EXPECT_EQ(1, gcd(359, 71));EXPECT_EQ(1, gcd(47, 83));
}TEST(FactorialTest, HandlesZeroInput) {EXPECT_EQ(factorial(0), 1);
}TEST(FactorialTest, HandlesPositiveInput) {EXPECT_EQ(factorial(1), 1);EXPECT_EQ(factorial(2), 2);EXPECT_EQ(factorial(3), 6);EXPECT_EQ(factorial(8), 40320);
}#if 0
int main(int argc, char *argv[])
{testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}
#endif

在上面的代码中包含了两组一共4个测试:

  1. 一组 GCDTest, 用于测试 gcd 函数
  2. 一组 FactorialTest 用于测试 factorial 函数。

2.3 编译并运行测试

然后使用下面的语句编译上面的代码生成可执行的测试文件:

$ g++ gcd.c factorial.c foo_unittest.cc -o footest -I. -Igtest/include -Lgtest/lib -lgtest -lgtest_main -lpthread

注意: 这里使用 g++ 指令一口气编译了 gcd.c, factorial.c 和 foo_unittest.cc 一共 3 个文件,生成测试文件 footest。

执行测试:

参考上面的操作,实际上只需要将 g++ 指令中的代码文件替换成你实际的文件就可以了。

2.4 关于测试文件的说明

在 gcd.c, factorial.c 和 foo_unittest.cc 中都没有定义 main 函数。

只在 foo_unittest.cc 中,有一个注释掉的 main 函数:

#if 0
int main(int argc, char *argv[])
{testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}
#endif

这个 main 函数是 GoogleTest 的入口,会调用 testing::InitGoogleTest(&argc, argv) 初始化 GoogleTest 框架,随后的 RUN_ALL_TESTS() 会搜集所有的测试函数,并运行。

实际上这也是 libtest_main.a 的所有代码,在 googletest v1.11 的代码中,完整的 gtest_main.cc 的代码如下:

/* file: googletest-src/googletest/src/gtest_main.cc */
#include <cstdio>
#include "gtest/gtest.h"#if GTEST_OS_ESP8266 || GTEST_OS_ESP32
#if GTEST_OS_ESP8266
extern "C" {#endif
void setup() {testing::InitGoogleTest();
}void loop() { RUN_ALL_TESTS(); }#if GTEST_OS_ESP8266
}
#endif#elseGTEST_API_ int main(int argc, char **argv) {printf("Running main() from %s\n", __FILE__);testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}
#endif

在上面的代码中,针对 GTEST_OS_ESP8266GTEST_OS_ESP32 的情形,单独定义了setuploop函数,其余的代码都走 else 部分的 main 函数。

实际上 setup + loop 函数的功能和 main 函数是一样的,我猜测是 GTEST_OS_ESP8266GTEST_OS_ESP32 默认有了 main 函数,所以这里无法再次定义 main 函数,于是将 googletest 的入口放入到 setuploop 函数中。

可见,foo_unittest.cc的 main 函数 和 gtest_main.cc 的代码是一样的。

如果自己定义了 main 函数,就不再需要链接 libgtest_main.a 了,否则的话就需要向上面那样在编译时链接上 libgtest_main.a

3. 总结

基于前面说得比较繁琐,这里将步骤总结如下:

  1. 保存文中 2.1 节的 CMakeLists.txt 和 gtest.mk
  2. 运行 make -f gtest.mk 会自动下载并编译输出 gtest 的库文件和头文件
  3. 执行如下编译指令编译并链接测试代码和 gtest 库文件
$ g++ gcd.c factorial.c foo_unittest.cc -o footest -I. -Igtest/include -Lgtest/lib -lgtest -lgtest_main -lpthread

命令中的 gcd.cfactorial.c 可以根据需要替换成一个或多个 C 代码文件, foo_unittest.cc 替换成自己的单元测试文件(里面包含了多组 TESTTEST_F测试)。

对于嵌入式环境,由于需要设置交叉编译环境来编译 googletest 库,然后链接成目标环境的可执行文件,相对这里的 x86 环境会稍微复杂一些,下篇单独开篇说明。

4. 广告

洛奇工作中常常会遇到自己不熟悉的问题,这些问题可能并不难,但因为不了解,找不到人帮忙而瞎折腾,往往导致浪费几天甚至更久的时间。

所以我组建了几个微信讨论群(记得微信我说加哪个群,如何加微信见后面),欢迎一起讨论:

  • 一个密码编码学讨论组,主要讨论各种加解密,签名校验等算法,请说明加密码学讨论群。
  • 一个Android OTA的讨论组,请说明加Android OTA群。
  • 一个git和repo的讨论组,请说明加git和repo群。

在工作之余,洛奇尽量写一些对大家有用的东西,如果洛奇的这篇文章让您有所收获,解决了您一直以来未能解决的问题,不妨赞赏一下洛奇,这也是对洛奇付出的最大鼓励。扫下面的二维码赞赏洛奇,金额随意:

洛奇自己维护了一个公众号“洛奇看世界”,一个很佛系的公众号,不定期瞎逼逼。公号也提供个人联系方式,一些资源,说不定会有意外的收获,详细内容见公号提示。扫下方二维码关注公众号:

两步实现在C代码中快速集成gtest进行单元测试相关推荐

  1. vscode 快速调到定义处_vim技巧:在程序代码中快速跳转,在文件内跳转到变量定义处...

    本篇文章介绍 vim 的一些使用技巧: 在程序代码中快速跳转 在文件内跳转到变量定义处 在程序代码中快速跳转 在 vim 中查看代码文件时,可以使用下面命令在程序代码中快速跳转,提高效率. % 跳转到 ...

  2. android快速点击两次,如何通过在Android中快速单击两次按钮来防...

    如果我在我的Android应用中快速单击按钮,似乎它后面的代码运行了两次. 如果我两次单击菜单按钮,则必须启动onclick的活动只会启动两次,而我必须退出两次. 这真的很烦人,因为如果我单击菜单按钮 ...

  3. 一步一步教你在IEDA中快速搭建SpringBoot项目

    场景 IEDA 2017 现在要在IDEA中搭建SpringBoot项目快速输出HelloWorld. 实现 打开IEDA,点击File--new--project 选择左边的Spring Initi ...

  4. 两步教你在Vue中设置登录验证拦截!

    Hello,你好呀,我是灰小猿,一个超会写bug的程序猿! 今天在做vue和springboot交互的一个项目的时候,想要基于前端实现一些只有登录验证之后才能访问某些页面的操作,所以在这里总结一下实现 ...

  5. 如何在游戏中快速集成聊天功能

    总览 本文以「人类跌落梦境」游戏为例,讲解如何在游戏场景下使用 LeanCloud IM SDK. LeanCloud 提供的即时通信 SDK 可以应用在多种场景,比如在线客服,直播间弹幕,工作群聊软 ...

  6. 在原有Android项目中快速集成React Native

    前言 对于现有的大多数项目来说都不是从头构建的,而要在原有项目的基础上引入React Native则肯定和用react-native init xxx创建工程不同.因此下面就来说下具体操作.不过在真正 ...

  7. 在MFC对话框中快速集成三维控件

    在MFC的对话框中可以方便的集成AnyCAD三维控件(c++版本),遵循一下几步: 1.在对话框资源中增加一个Static控件,ID为IDC_STATIC_3D,并且把它的Notify属性设置为Tru ...

  8. 在网页中快速集成自己的即时通聊天,实现类是淘宝旺旺的在线洽谈效果。

    集成简单: 在网页中集成聊天工具能为用户提供在线沟通交流平台的程序,让客户无需安装快速沟通.为不同用户之间构建起在线聊天沟通的对话桥梁. 集成简单: 可以与任何语言进行集成,无二次开发门槛,只需简单的 ...

  9. 地理计算 | EXCEL中快速计算列表的经纬度距离

    前言 物流配送.城市通勤.测绘外业勘察等场景,经常使用EXCEL软件作为数据处理工具软件,在表格中记录经纬度列表,例如下图表格每行记录一个经纬度坐标,表示运动轨迹的坐标.根据业务要求需快速计算上下两个 ...

最新文章

  1. 一、nginx基本模块以及模块配置
  2. linux中crontab命令的基本用法
  3. NDK 编译armebai-v7a的非4字节对齐crash Fatal signal 7 (SIGSEGV) 错误解决
  4. linux天气软件,类似智能手机!Linux中安装Conky天气插件
  5. 字节跳动想取消大下周,遭到部分员工激烈反对
  6. Vue笔记大融合总结
  7. Java学习笔记之log4j与commons-logging转
  8. Downloading SRA data using command line utilities
  9. ORACLE新增DATABASE LINK
  10. 说一下syslog日志吧~~~
  11. 快速排序 JAVA实现
  12. FPM一:简单的road map(GAF)
  13. 一个问题讨论:为什么有些境外和港澳台地区的手机APP打不开
  14. html注册页面带验证码,登陆注册-带图片的验证码
  15. win10不让桌面上显示宽带连接服务器,Win10宽带连接桌面看不见了怎么办?
  16. Java语言,从入门到放弃
  17. HihoCoder - 1829 Tomb Raider (暴力+最长上升子序列)
  18. 11.什么是Heuristic
  19. Kaggle -Linear Regression with Time Series
  20. Rust-WebAssembly 开发者布道师招聘

热门文章

  1. nokia 6300手机QQ4.0下载,设置空间支持jar下载
  2. word与EndNote管理文献~引文格式问题
  3. 自媒体平台今日头条申请秘籍(转)
  4. RMAN duplicate 方式 做个备库
  5. 自己尝试使用简单数据集实现决策树 代码——《机器学习实战》
  6. 电子招投标系统源码之了解电子招标投标全流程
  7. 源码包安装Nginx(1.19.1),并配置Nginx,比如:用户认证,防盗链,虚拟主机,SSL等功能
  8. 对List中的map的key按中文拼音进行排序。
  9. 经典测试用例,一个水杯的测试
  10. 用微PE工具箱安装系统