0. 前言
-
要转型成为C++工程师,要走的路还很长啊……最近写了不少C++代码和项目,单元测试都是自己随便写写,不成体系,希望能够了解下业界的写法,自己也学一学。
-
单元测试框架好像没有什么悬念,就是googletest了。
-
官方资料阅读
1. Getting Started
-
主要内容
- GTest的安装平台要求
- Bazel 引入 GTest
- CMake 引入 GTest,对应文档
- 修改
CMakeLists.txt
- 添加源文件
- 修改
-
CMakeLists.txt
相关内容主要包括- 引入 GTest:教程里比较骚,用的是 CMake 的
FetchContent_Declare/FetchContent_MakeAvailable
,直接从github下载源码并解析。 - 开启Test,引入GTest头文件、so以及相关测试源码。
- 引入 GTest:教程里比较骚,用的是 CMake 的
-
测试源码:就是一些GTest的断言。
2 Googletest Primer
-
官方推荐的基础教程,网址是这里
-
GoogleTest 有什么作用
- 测试应该是独立,且可重复的。
- 测试应该有很好的组织结构,能反映被测试代码的总体结构。
- 测试应该可移植、可复用
- 当测试失败时,应该提供足够的信息
- 单元测试的速度应该足够快
-
GoogleTest 中用到的一些术语以及基本概念
- assertions:断言,判断一个条件是否为真。断言的结果可以是 success/non fatal failure/fatal failure,如果是 fatal failure 则就会跳出程序。
- Test:使用 assertions 来判断程序是否运行成功。
- Test Case:从现在来看,应该是 Test 的同义词。
- 然而,由于历史原因,GoogleTest中以前Test Case其实是 Test Suite的含义,但现在正在往 Test Suite 上转型。
- Test Suite:包含一个或多个 Test。应该通过 Test Suite 来管理 Test,并反映程序的总体结构。
- Test program:包含多个 Test Suites。
-
Assertions
- GoogleTest中的断言,是一系列宏定义,通过一个断言来判断类、函数的行为是否符合预期。
- 每一类断言都有两个对应的宏,一个形如
ASSERT_*
,如果断言报错则中断当前函数;一个形如EXCEPT_*
,如果报错不会中断当前函数。 - 如何构建自定义错误消息,就是在宏后面添加
<<
并跟上错误内容,比如ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";
-
编写
TEST
- 注意,这里的
TEST
指的是宏,也就是上面基本概念中的 Test Case。 - 基本语法以及举例如下
- 两个参数必须是合法的C++变量名
- 注意,这里的
TEST(TestSuiteName, TestName) {
... test body ...
}
// Tests factorial of 0.
TEST(FactorialTest, HandlesZeroInput) {
EXPECT_EQ(Factorial(0), 1);
}
// Tests factorial of positive numbers.
TEST(FactorialTest, HandlesPositiveInput) {
EXPECT_EQ(Factorial(1), 1);
EXPECT_EQ(Factorial(2), 2);
EXPECT_EQ(Factorial(3), 6);
EXPECT_EQ(Factorial(8), 40320);
}
- 如果若干个Test Case 要公用参数,按就需要使用 Test Fixtures
- 总体流程如下:
- 构建一个类,继承
::testing::Test
- 需要继承的成员变量(测试中用到的各种参数)都放在 protected 里面
- 使用
SetUp
作为初始化参数的方法。 - 如果需要,也可以定义成员变量,在各种测试中复用。
- 使用
TEST_F
解决
- 构建一个类,继承
- 总体流程如下:
class QueueTest : public ::testing::Test {
protected:
void SetUp() override {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// void TearDown() override {}
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
TEST_F(QueueTest, IsEmptyInitially) {
EXPECT_EQ(q0_.size(), 0);
}
TEST_F(QueueTest, DequeueWorks) {
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;
}
-
启动测试
- 基本流程:
- 通过
TEST
以及TEST_F
注册所有测试。注意,不需要另外罗列所有测试。 - 通过
RUN_ALL_TESTS()
执行测试。返回值为0则表示成功,1表示失败。
- 通过
- 基本流程:
-
一个实例
#include "this/package/foo.h"
#include "gtest/gtest.h"
namespace my {
namespace project {
namespace {
// The fixture for testing class Foo.
class FooTest : public ::testing::Test {
protected:
// You can remove any or all of the following functions if their bodies would
// be empty.
FooTest() {
// You can do set-up work for each test here.
}
~FooTest() override {
// You can do clean-up work that doesn't throw exceptions here.
}
// If the constructor and destructor are not enough for setting up
// and cleaning up each test, you can define the following methods:
void SetUp() override {
// Code here will be called immediately after the constructor (right
// before each test).
}
void TearDown() override {
// Code here will be called immediately after each test (right
// before the destructor).
}
// Class members declared here can be used by all tests in the test suite
// for Foo.
};
// Tests that the Foo::Bar() method does Abc.
TEST_F(FooTest, MethodBarDoesAbc) {
const std::string input_filepath = "this/package/testdata/myinputfile.dat";
const std::string output_filepath = "this/package/testdata/myoutputfile.dat";
Foo f;
EXPECT_EQ(f.Bar(input_filepath, output_filepath), 0);
}
// Tests that Foo does Xyz.
TEST_F(FooTest, DoesXyz) {
// Exercises the Xyz feature of Foo.
}
} // namespace
} // namespace project
} // namespace my
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}