Skip to content
Published at:

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

cpp
#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.alibgtest.a区别:

如果写多个测试程序,会发现main函数中的初始化和运行是重复的,libgtestmain.alibgtest.a也是多的这一部分;如果不同写main函数这部分代码,程序链接libgtestmain.a即可

主要断言宏

cpp
// 显示指定成功或失败
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

支持流操作符

当失败时,可以打印更多信息

cpp
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

cpp
ASSERT_NE(n, nullptr)

测试夹具

上面只是一些简单的测试用途,对于有些复杂的情况:比如在断言前我需要去初始化数据xxx,然后测试,测试完了需要释放资源;并且这样的数据我希望在多个测试case中共享;如下:

c++
TEST(IsPrimeTest, Positive) {
		// 1. init data
		// do something

		// 2. test data
    EXPECT_FALSE(xxx);
    EXPECT_TRUE(xxx);

    // 3. release data
    // do something
}

问题:给不能然我只专注于中间的第二步,测试断言,数据的初始化和释放抽离出去放在别的地方,同时这个数据可以提供给多个测试case共享。答案是测试夹具

使用步骤:

  1. 写一个类继承自 ::testing::Test
  2. 重写protected修饰的 SetUp()TearDown()函数
  3. 如果需要可以定义一些子程序,用于共享
c++
#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
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
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
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)

命令:

bash
$ 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插件:

可用鼠标点击运行,可以调试,可以查看log

Updated at: