【C++框架#1】gflags 和 gtest 安装使用
一、gflags 安装和使用
1. 概述
介绍:gflags 是 Google 开发的一个开源库,用于 C++ 应用程序中命令行参数的声明、定义和解析。gflags 库提供了一种简单的方式来添加、解析和文档化命令行标志(flags),使得程序可以根据不同的运行时配置进行调整。
它具有如下几个特点:
- 易于使用:gflags 提供了一套简单直观的 API 来定义和解析命令行标志,使得开发者可以轻松地为应用程序添加新的参数。
- 自动帮助和文档:gfags 可以自动生成每个标志的帮助信息和文档,这有助于用户理解如何使用程序及其参数。
- 类型安全:gflags 支持多种数据类型的标志,包括布尔值、整数、字符串等,并且提供了类型检查和转换。
- 多平台支持:gflags 可以在多种操作系统上使用,包括Windows、Linux 和macOS
- 可扩展性:gflags 允许开发者自定义标志的注册和解析逻辑,提供了强大的扩展性
官方文档: https://gflags.github.io/gflags/
代码仓库:https://github.com/gflags/gflags.git
安装
sudo apt-get install libgflags-dev# 查看目录下文件
lighthouse@VM-8-10-ubuntu:~$ ls /usr/include/gflags
gflags_completions.h gflags_declare.h gflags_gflags.h gflags.h
2. 使用
-
定义参数:利用 gflag 提供的宏定义来定义参数。该宏的3个参数分别为命令行参数名,参数默认值,参数的帮助信息。
DEFINE_bool(reuse_addr, true, "是否开始网络地址重用选项"); DEFINE_int32(log_level, 1, "日志等级:1-DEBUG, 2-WARN, 3-ERROR"); DEFINE_string(log_file, "stdout", "日志输出位置设置,默认为标准输出");
-
访问参数:可以在程序中通过 FLAGS name 像正常变量一样访问标志参数。比如在上面的例子中,可以通过 FLAGS big menu 和 FLAGS languages 变量来访问命令行参数。
-
不同文件访问参数:如果想再另外一个文件访问当前文件的参数,以参数 FLAGS big_menu 为例,可以使用用宏
DECLARE bool(big menu)
来声明引入这个参数。其实这个宏就相当于做了 extern FLAGS big menu, 定义外部链接属性。 -
初始化所有参数:当定义好参数后,需要告诉可执行程序去处理解析命令行传入的参数,使得FLAGS*变量能得到正确赋值。我们需要在 main 函数中,调用下面的函数来解决命令行传入的所有参数。
google::ParseCommandLineFlags(&argc, &argv, true);
- argc 和 argv 就是 main 的入口参数
- 第三个参数被称为
remove flags
。如果它为 true,表示ParsecommandLineFlags 会从 argv 中移除标识和它们的参数,相应减少 argc 的值如果它为 false,ParsecommandLineFlags 会保留 argc 不变,但将会重新调整它们的顺序,使得标识再前面。
-
运行参数设置:gflags 为我们提供了多种命令行设置参数的方式。string 和 int 设置参数
exec --log file= "./main.log" exec -log file= "./main.log" exec --log file "./main.log" exec -log file "./main.log"
bool 设置参数
exec --reuse addr exec --noreuse addr exec --reuse addr=true exec --reuse addr=false
-
将会终止标识的处理。比如在exec-f11—f22中,f1 被认为是一个标识但f2 不会 -
配置文件使用:配置文件的使用,其实就是为了让程序的运行参数配置更加标准化,不需要每次运行的时候都手动收入每个参数的数值,而是通过配置文件,一次编写,永久使用。需要注意的是,配置文件中选项名称必须与代码中定义的选项名称一致。
-
特殊参数标识:gflags 默认为我们提供一些特殊标识
--help
:显示文件中所有标识的帮助信息--helpfull
:和-help 一样,帮助信息更全面一些--helpshort
:只显示当前执行文件里的标志--helpxml
:以xml 方式打印,方便处理--version
:打印版本信息,由google::SetVersionstring()
设定--flagfile -flagfile=f
:从文件 f 中读取命令行参数
案例如下
main.cc
#include <gflags/gflags.h>
#include <iostream>DEFINE_string(ip, "127.0.0.1", "这是服务器的监听IP地址, 格式: 127.0.0.1");
DEFINE_int32(port, 8080, "这是服务器的监听端口, 格式: 8080");
DEFINE_bool(debug_enable, true, "是否启用调用模式, 格式: true/false");int main(int argc, char* argv[]){google::ParseCommandLineFlags(&argc, &argv, true);std::cout << "FLAGS_ip: " << FLAGS_ip << ", port: " << FLAGS_port << std::endl;std::cout << "debug_enable: " << FLAGS_debug_enable << std::endl;return 0;
}
Makefile 文件
main:main.ccg++ -o $@ $^ -std=c++17 -lgflags
配置文件编写
# main.conf 代码如下
-ip="192.168.1.1"
-port=10001
-debug_enable=true# 测试
lighthouse@VM-8-10-ubuntu:gflags$ ./main --flagfile main.conf
FLAGS_ip: "192.168.1.1" , port: 10001
debug_enable: 1
样例运行
# 输出
lighthouse@VM-8-10-ubuntu:gflags$ ./main
FLAGS_ip: 127.0.0.1, port: 8080
debug_enable: 1
lighthouse@VM-8-10-ubuntu:gflags$ ./main --ip="192.168.1.1" --port=9090 --debug_enable=false
FLAGS_ip: 192.168.1.1, port: 9090
debug_enable: 0# 其他
lighthouse@VM-8-10-ubuntu:gflags$ ./main --helpFlags from main.cc:-debug_enable (是否启用调用模式, 格式: true/false) type: booldefault: true-ip (这是服务器的监听IP地址, 格式: 127.0.0.1) type: stringdefault: "127.0.0.1"-port (这是服务器的监听端口, 格式: 8080) type: int32default: 8080
二、gtest 安装和使用
1. 概述
介绍:GTest 是一个跨平台的 C++单元测试框架,由 google 公司发布。gtest是为了在不同平台上为编写 C++单元测试而生成的。它提供了丰富的断言、致命和非致命判断、参数化等等测试所需的宏,以及全局测试,单元测试组件。
安装
sudo apt-get install libgtest-dev
2. 使用
TEST 宏
- TEST(测试名,测试样例名):主要用来创建一个简单测试,它定义了一个测试函数,在这个函数中可以使用任何 C++代码并且使用框架提供的断言进行检查
- TEST_F(test_fixture, test_name): 主要用来进行多样测试,适用于多个测试场景如果需要相同的数据配置的情况, 即相同的数据测不同的行为
断言宏
- ASSERT 系列:如果当前点检测失败则退出当前函数
- EXPECT 系列:如果当前点检测失败则继续往下执行
下面是经常使用的断言介绍
bool 值检查
ASSERT_TRUE
(参数):期待结果是 trueASSERT_FALSE
(参数):期待结果是 false
数值型数据检查
ASSERT_EQ
(参数 1,参数 2):传入的是需要比较的两个数 equalASSERT_NE
(参数 1,参数 2):not equal,不等于才返回 trueASSERT_LT
(参数1,参数2):less than,小于才返回 trueASSERT_GT
(参数1,参数2):greater than,大于才返回 trueASSERT_LE
(参数 1,参数2):less equal,小于等于才返回 trueASSERT_GE
(参数 1,参数 2):greater equal,大于等于才返回 true
样例代码如下
#include <gtest/gtest.h>
#include <iostream>int Add(int num1, int num2){return num1 + num2;
}
TEST(测试名, 测试用例名){ASSERT_EQ(Add(10, 20), 30);ASSERT_LT(Add(20, 20), 30);
}
TEST(测试名, 字符串比较测试){std::string str = "hello";ASSERT_EQ(str, "hello");printf("断言失败后的打印\n");ASSERT_EQ(str, "Hello");
}int main(int argc, char* argv[]){// 1. 单元框架的初始化testing::InitGoogleTest(&argc, argv);// 2. 开始单元测试return RUN_ALL_TESTS();
}
结果如下:
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from 测试名
[ RUN ] 测试名.测试用例名
main.cc:9: Failure
Expected: (Add(20, 20)) < (30), actual: 40 vs 30
[ FAILED ] 测试名.测试用例名 (0 ms)
[ RUN ] 测试名.字符串比较测试
断言失败后的打印
main.cc:15: Failure
Expected equality of these values:strWhich is: "hello""Hello"
[ FAILED ] 测试名.字符串比较测试 (0 ms)
[----------] 2 tests from 测试名 (1 ms total)[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (2 ms total)
[ PASSED ] 0 tests.
[ FAILED ] 2 tests, listed below:
[ FAILED ] 测试名.测试用例名
[ FAILED ] 测试名.字符串比较测试
3. 事件机制
定义:指在 测试前 和 测试后 提供给用户自行添加操作的机制,而且该机制也可以让同一测试套件下的测试用例 共享数据。
GTest框架中事件的结构层次
- 测试程序:一个测试程序只有一个 main 函数,也可以说是一个可执行程序是一个测试程序。该级别的事件机制是在程序的开始和结束执行
- 测试套件:代表一个测试用例的集合体,该级别的事件机制是在整体的测试案例开始和结束执行
- 测试用例:该级别的事件机制是在每个测试用例开始和结束都执行
事件机制的最大好处就是能够为我们各个测试用例提前准备好测试环境,并在测试完毕后用于销毁环境,这样有个好处就是如果我们有一端代码需要进行多种不同方法的测试,则可以通过测试机制在每个测试用例进行之前初始化测试环境和数据,并在测试完毕后清理测试造成的影响。
GTest 提供了三种常见的事件
3.1 全局事件
针对整个测试程序。实现全局的事件机制,在测试之前配置测试环境数据,测试完毕后清理数据
- 首先需要创建一个自己的类,通过继承
testing::.Environment
派生类来完成 - 分别实现成员函数
SetUp
和TearDown
,同时在 main函数内进行调用testing::AddGlobalTestEnvironment(new MyEnvironment);
函数添加全局的事件机制 - 重写的虚函数接口
SetUp
在测试前被调用,TearDown
在测试完毕后被调用
样例代码
#include <gtest/gtest.h>
#include <iostream>std::unordered_map<std::string, std::string> dict;class HashTestEnv: public testing::Environment{
public:virtual void SetUp() override{std::cout << "测试前: 准备数据\n";dict.insert(std::make_pair("hello", "你好"));dict.insert(std::make_pair("qian", "你好"));dict.insert(std::make_pair("xuan", "你好"));}virtual void TearDown() override{std::cout << "测试结束: 清理数据\n";dict.clear();}
};TEST(hash_case_test, find_test){auto it = dict.find("hello");ASSERT_NE(it, dict.end());
}
TEST(hash_case_test, size_test){ASSERT_GT(dict.size(), 0);
}int main(int argc, char* argv[]){testing::AddGlobalTestEnvironment(new HashTestEnv);testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}
输出
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
测试前: 准备数据
[----------] 2 tests from hash_case_test
[ RUN ] hash_case_test.find_test
[ OK ] hash_case_test.find_test (0 ms)
[ RUN ] hash_case_test.size_test
[ OK ] hash_case_test.size_test (0 ms)
[----------] 2 tests from hash_case_test (0 ms total)[----------] Global test environment tear-down
测试结束: 清理数据
[==========] 2 tests from 1 test suite ran. (2 ms total)
[ PASSED ] 2 tests.
3.2 TestSuite 事件
针对 套件/集合 进行单元测试,即 将多个相关测试归入一组的方式进行测试,为这组测试用例进行环境配置和清理
-
对一个功能的验证往往需要很多测试用例,测试套件就是针对一组相关测试用例进行 环境配置 的事件机制
-
定义环境类,继承
testing::Test
基类,实现两个静态函数SetUpTestcase
和TearDownTestcase
-
注意:测试套件的事件机制不需要像全局事件机制一样在 main 注册,而是需要将我们平时使用的TEST 宏改为 TEST F宏。
-
SetUpTestCase()
函数是在测试套件第一个测试用例开始前执行,TearDownTestCase()
函数是在测试套件最后一个测试用例结束后执行
需要注意 TEST_F
的第一个参数是我们创建的类名,也就是当前测试套件的名称,这样在 TEST_F
宏的测试套件中就可以访问类中的成员了。
样例代码
class HashTestEnv: public testing::Test {
public: static void SetUpTestCase() { std::cout << "第一个 TEST 之前调用\n"; } static void TearDownTestCase() { std::cout << "最后一个 TEST 之后调用\n"; } std::unordered_map<std::string, std::string> dict;
}; TEST_F(HashTestEnv, insert_test){dict.insert(std::make_pair("qian", "你好"));dict.insert(std::make_pair("xuan", "你好"));auto it = dict.find("qian");ASSERT_NE(it, dict.end());
}
TEST_F(HashTestEnv, size_test){std::cout << "中间 size 测试\n";ASSERT_GT(dict.size(), 0);
}int main(int argc, char* argv[]){testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}
输出
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from HashTestEnv
第一个 TEST 之前调用
[ RUN ] HashTestEnv.insert_test
[ OK ] HashTestEnv.insert_test (0 ms)
[ RUN ] HashTestEnv.size_test
中间 size 测试
main.cc:24: Failure
Expected: (dict.size()) > (0), actual: 0 vs 0
[ FAILED ] HashTestEnv.size_test (0 ms)
最后一个 TEST 之后调用
[----------] 2 tests from HashTestEnv (0 ms total)[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (1 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] HashTestEnv.size_test1 FAILED TEST
能够看到在上例中,有一个好处,就是将数据与测试结合到同一个测试环境类中了这样与外界的耦合度更低,代码也更清晰。
但是同样的,我们发现在两个测试用例中第二个测试用例失败了,这是为什么呢?这就涉及到了 TestCase 事件的机制。
3.3 TestCase 事件
针对一个个测试用例。测试用例的事件机制的创建和测试套件的基本一样,不同地方在于测试用例实现的两个函数分别是 Setup 和 TearDown,这两个函数也不是静态函数
SetUp()
函数是在一个测试用例的开始前执行TearDown()
函数是在一个测试用例的结束后执行
也就是说,在 TestSuite/TestCase 事件中,每个测试用例,虽然它们同用同一个事件环境类,可以访问其中的资源,但是本质上每个测试用例的环境都是独立的,这样我们就不用担心不同的测试用例之间会有数据上的影响了,保证所有的测试用例都使用相同的测试环境进行测试。
样例代码
#include <gtest/gtest.h>
#include <iostream>class HashTestEnv: public testing::Test {
public: static void SetUpTestCase() { std::cout << "第一个 TEST 之前被调用,进行总体环境配置\n"; } static void TearDownTestCase() { std::cout << "最后一个 TEST 之后被调用,进行总体环境清理\n"; } virtual void SetUp() override{ std::cout << "测试前: 提前准备数据!!\n"; dict.insert(std::make_pair("bye", "再见")); dict.insert(std::make_pair("see you", "再见")); } virtual void TearDown() override{ std::cout << "测试结束: 清理数据!!\n"; dict.clear();}std::unordered_map<std::string, std::string> dict;
}; TEST_F(HashTestEnv, insert_test) { std::cout << "中间测试\n"; dict.insert(std::make_pair("hello", "你好")); ASSERT_EQ(dict.size(), 3);
}
TEST_F(HashTestEnv, size_test) { std::cout << "中间 size 测试\n"; auto it = dict.find("hello"); ASSERT_EQ(it, dict.end()); ASSERT_EQ(dict.size(), 2);
} int main(int argc, char *argv[]) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();
}
输出
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from HashTestEnv
第一个 TEST 之前被调用,进行总体环境配置
[ RUN ] HashTestEnv.insert_test
测试前: 提前准备数据!!
中间测试
测试结束: 清理数据!!
[ OK ] HashTestEnv.insert_test (0 ms)
[ RUN ] HashTestEnv.size_test
测试前: 提前准备数据!!
中间 size 测试
测试结束: 清理数据!!
[ OK ] HashTestEnv.size_test (0 ms)
最后一个 TEST 之后被调用,进行总体环境清理
[----------] 2 tests from HashTestEnv (0 ms total)[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (1 ms total)
[ PASSED ] 2 tests.