googletest上手
大纲:
- 为什么需要写单元测试
- googletest简介
- googletest中的概念
- hello world程序
- 主要断言宏
- 测试夹具
- googletest整合到项目
- googletest和CMake整合
- googletest和VSCode整合
为什么需要写单元测试
SQLite数据库是个很好的例子:数据库代码是14W行,测试代码和测试脚本9191.1W行。https://www.sqlite.org/testing.html
- 让问题bug更早的显现
- 让你对你的代码更有信心
googletest简介
https://github.com/google/googletest
什么是一个好的测试?
- 测试应该是独立的和可重复的
- 测试应该很好地“组织”,并反映出测试代码的结构
- 测试应该是“可移植的”和“可重用的”
- 测试框架应该将测试编写者从日常琐事中解放出来,让他们专注于测试“内容”
- 测试应该是“快速的”
googletest中的概念
测试程序 --> 测试suite --> 测试case
上面之间是1:N的关系,一个测试程序可以有一个或多个测试suite,一个测试suite可以有一个或多个测试case。类比的可以理解为,测试suite是你要测试的一个函数(功能),测试case对应这个函数的不同
googletest代码编译产生四个库:
- libgtest.a
- libgtest_main.a
- libgmock.a
- libgmock_main.a
libgtest_main.a
里面包含libgtest.a
,如果代码链接了libgtest_main.a
就不用去链接libgtest.a
,其中main
的意思是这个静态库包含了main函数,在写测试代码时不需要自己去写main函数了,专注去去写测试case;gmock库包含了gtest的内容;使用时只需要链接其中一个库即可;
断言Assertions
ASSERT_*
系列:失败时会生成fatal错误,同时会终止当前的函数EXPECT_*
系列:失败时会生成nonfatal错误,不会终止当前的函数
hello world程序
步骤:
- 编写代码(后续需要测试的代码)
- 引入gtest头文件
- 在main函数中初始化gtest
- 链接libgtest.a库
- 运行程序
示例基于https://github.com/google/googletest/blob/master/googletest/samples/sample1_unittest.cc 改
#include <iostream>
#include "gtest/gtest.h"
// 判断是否是质数:被1和自己本身整除的数
bool IsPrime(int n) {
if (n <= 1) return false;
if (n % 2 == 0) return n == 2;
for (int i = 3;; i += 2) {
if (i > n / i) break;
if (n % i == 0) return false;
}
return true;
}
// Tests negative input.
TEST(IsPrimeTest, Negative) {
EXPECT_FALSE(IsPrime(-1));
EXPECT_FALSE(IsPrime(-2));
EXPECT_FALSE(IsPrime(INT_MIN));
}
// Tests some trivial cases.
TEST(IsPrimeTest, Trivial) {
EXPECT_FALSE(IsPrime(0));
EXPECT_FALSE(IsPrime(1));
EXPECT_TRUE(IsPrime(2));
EXPECT_TRUE(IsPrime(3));
}
// Tests positive input.
TEST(IsPrimeTest, Positive) {
EXPECT_FALSE(IsPrime(4));
EXPECT_TRUE(IsPrime(5));
EXPECT_FALSE(IsPrime(6));
EXPECT_TRUE(IsPrime(23));
}
// init
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
libgtestmain.a
与libgtest.a
区别:
如果写多个测试程序,会发现main函数中的初始化和运行是重复的,libgtestmain.a
比libgtest.a
也是多的这一部分;如果不同写main函数这部分代码,程序链接libgtestmain.a
即可
主要断言宏
// 显示指定成功或失败
SUCCEED()
FAIL()
switch(expression) {
case 1:
... some checks ...
case 2:
... some other checks ...
default:
FAIL() << "We shouldn't get here.";
}
// boolean条件
// EXPECT_TRUE
EXPECT_TRUE(condition)
ASSERT_TRUE(condition)
// EXPECT_FALSE
EXPECT_FALSE(condition)
ASSERT_FALSE(condition)
// 二元比较
// EXPECT_EQ 相等 equal
EXPECT_EQ(val1,val2)
ASSERT_EQ(val1,val2)
// EXPECT_NE 不等于 not equal
EXPECT_NE(val1,val2)
ASSERT_NE(val1,val2)
// EXPECT_LT 小于 less than
EXPECT_LT(val1,val2)
ASSERT_LT(val1,val2)
// EXPECT_LE 小于等于less equal
EXPECT_LE(val1,val2)
ASSERT_LE(val1,val2)
// EXPECT_GT 大于 greater than
EXPECT_GT(val1,val2)
ASSERT_GT(val1,val2)
// EXPECT_GE 大于等于 greater than
EXPECT_GE(val1,val2)
ASSERT_GE(val1,val2)
// 字符串比较
// EXPECT_STREQ 相等
EXPECT_STREQ(str1,str2)
ASSERT_STREQ(str1,str2)
// EXPECT_STRNE 不相等
EXPECT_STRNE(str1,str2)
ASSERT_STRNE(str1,str2)
// EXPECT_STRCASEEQ
EXPECT_STRCASEEQ(str1,str2)
ASSERT_STRCASEEQ(str1,str2)
// EXPECT_STRCASENE
EXPECT_STRCASENE(str1,str2)
ASSERT_STRCASENE(str1,str2)
// 浮点数比较
// EXPECT_FLOAT_EQ 单精度相等
EXPECT_FLOAT_EQ(val1,val2)
ASSERT_FLOAT_EQ(val1,val2)
// EXPECT_DOUBLE_EQ 双精度相等
EXPECT_DOUBLE_EQ(val1,val2)
ASSERT_DOUBLE_EQ(val1,val2)
// EXPECT_NEAR
EXPECT_NEAR(val1,val2,abs_error)
ASSERT_NEAR(val1,val2,abs_error)
// 谓词断言Predicate Assertions:失败里打印更复杂的清晰错误信息
// EXPECT_PRED*
EXPECT_PRED1(pred,val1) // pred是函数名(地址),后面的val*是函数参数
EXPECT_PRED2(pred,val1,val2)
EXPECT_PRED3(pred,val1,val2,val3)
EXPECT_PRED4(pred,val1,val2,val3,val4)
EXPECT_PRED5(pred,val1,val2,val3,val4,val5)
ASSERT_PRED1(pred,val1)
ASSERT_PRED2(pred,val1,val2)
ASSERT_PRED3(pred,val1,val2,val3)
ASSERT_PRED4(pred,val1,val2,val3,val4)
ASSERT_PRED5(pred,val1,val2,val3,val4,val5)
bool MutuallyPrime(int m, int n) { ... }
...
const int a = 3;
const int b = 4;
const int c = 10;
...
EXPECT_PRED2(MutuallyPrime, a, b); // Succeeds
EXPECT_PRED2(MutuallyPrime, b, c); // Fails
支持流操作符
当失败时,可以打印更多信息
ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";
for (int i = 0; i < x.size(); ++i) {
EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
}
使用nullptr而非NULL
与NULL的本质有关,避免出错;NULL的定义#define NULL 0
ASSERT_NE(n, nullptr)
测试夹具
上面只是一些简单的测试用途,对于有些复杂的情况:比如在断言前我需要去初始化数据xxx,然后测试,测试完了需要释放资源;并且这样的数据我希望在多个测试case中共享;如下:
TEST(IsPrimeTest, Positive) {
// 1. init data
// do something
// 2. test data
EXPECT_FALSE(xxx);
EXPECT_TRUE(xxx);
// 3. release data
// do something
}
问题:给不能然我只专注于中间的第二步,测试断言,数据的初始化和释放抽离出去放在别的地方,同时这个数据可以提供给多个测试case共享。答案是测试夹具
使用步骤:
- 写一个类继承自
::testing::Test
- 重写protected修饰的
SetUp()
和TearDown()
函数 - 如果需要可以定义一些子程序,用于共享
#include <iostream>
#include "gtest/gtest.h"
#include "queue.h" // https://github.com/google/googletest/blob/master/googletest/samples/sample3-inl.h
class QueueTest : public ::testing::Test {
protected:
void SetUp() override {
std::cout << "SetUp " << std::endl;
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
void TearDown() override {
std::cout << "TearDown " << std::endl;
}
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
TEST_F(QueueTest, IsEmptyInitially) {
std::cout << "IsEmptyInitially" << std::endl;
EXPECT_EQ(q0_.Size(), 0);
}
TEST_F(QueueTest, DequeueWorks) {
std::cout << "DequeueWorks" << std::endl;
int* n = q0_.Dequeue();
EXPECT_EQ(n, nullptr);
n = q1_.Dequeue();
ASSERT_NE(n, nullptr);
EXPECT_EQ(*n, 1);
EXPECT_EQ(q1_.Size(), 0);
delete n;
n = q2_.Dequeue();
ASSERT_NE(n, nullptr);
EXPECT_EQ(*n, 2);
EXPECT_EQ(q2_.Size(), 1);
delete n;
}
int main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
- 使用TEST_F编写测试case
- 测试suite名需要和类名相同(QueueTest)
- 重写SetUp和TearDown函数,SetUp在测试case前调用,TearDown在测试case后调用
- 可以在测试case中直接使用QueueTest类中的成员变量
./gtest_demo1
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from QueueTest
[ RUN ] QueueTest.IsEmptyInitially
SetUp
IsEmptyInitially
TearDown
[ OK ] QueueTest.IsEmptyInitially (0 ms)
[ RUN ] QueueTest.DequeueWorks
SetUp
DequeueWorks
TearDown
[ OK ] QueueTest.DequeueWorks (0 ms)
[----------] 2 tests from QueueTest (0 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 2 tests.
googletest整合到项目
方式:
- 利用CMake的模块下载
- 拷贝googletest源码到项目中编译
利用CMake的模块下载:
cmake_minimum_required(VERSION 3.14)
project(my_project)
# GoogleTest requires at least C++11
set(CMAKE_CXX_STANDARD 11)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
拷贝googletest源码到项目中编译:
cmake_minimum_required(VERSION 3.0.0)
project(gtest_demo VERSION 0.1.0)
# 编译googletest
add_subdirectory("third-parts/googletest")
add_executable(gtest_demo main.cpp)
target_link_libraries(gtest_demo
PUBLIC gtest
)
googletest和CMake整合
步骤:
- 开启ctest
- 程序链接gtest
- 使用GoogleTest模块
cmake_minimum_required(VERSION 3.0.0)
project(gtest_demo VERSION 0.1.0)
add_subdirectory("third-parts/googletest")
# 开启ctest
include(CTest)
enable_testing()
add_executable(gtest_demo main.cpp)
# 程序链接gtest
target_link_libraries(gtest_demo
PUBLIC gtest
)
# 使用GoogleTest模块
include(GoogleTest)
gtest_discover_tests(gtest_demo)
命令:
$ cmake -S . -B build
$ cmake --build build
$ cd build
$ ctest
Test project /Users/shibin/code/gtest_demo/build
Start 1: IsPrimeTest.Negative
1/3 Test #1: IsPrimeTest.Negative ............. Passed 0.01 sec
Start 2: IsPrimeTest.Trivial
2/3 Test #2: IsPrimeTest.Trivial .............. Passed 0.01 sec
Start 3: IsPrimeTest.Positive
3/3 Test #3: IsPrimeTest.Positive ............. Passed 0.01 sec
100% tests passed, 0 tests failed out of 3
Total Test time (real) = 0.03 sec
googletest和VSCode整合
VSCode插件:
- Test Explorer UI:https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer
- C++ TestMate:https://marketplace.visualstudio.com/items?itemName=matepek.vscode-catch2-test-adapter
- Debug插件:CodeLLDB或C/C++,用于调试测试
可用鼠标点击运行,可以调试,可以查看log