1、单元测试
单元测试unit testing
是指对软件中的最小可测试单元进行检查和验证,包括函数、类、模块、复杂交互逻辑等。gtest 中单元测试以单例模式实现。每个单元测试包含若干个测试套件test suite
,测试套件是指一组功能相同的测试脚本或过程。每个测试套件包含多个测试案例test case
,测试同一个功能的不同方向。
根据 gtest 官方文档,一个好的单元测试应该满足:
- 独立、可重复:测试案例可单独执行,错误可重复发生。
- 反映测试代码结构:功能完整,体现完备逻辑。对于关联功能,一个功能是否影响其他功能
- 可移植、可复用:跨平台。
- 尽可能多的出错信息:不会因为一次失败而停止,会继续测试下一个测试案例。一次测试发现多个错误。
- 自动跟踪所有测试而无需枚举:向容器中添加
- 测试高效:测试间重用资源,
mock
模拟复杂交互
如何整体使用单元测试
- 每个类或功能块加上测试案例
- test 目录加上所有的测试
- 一个单元测试可能散落在多个文件
单元测试中的打桩,是指用来代替关联代码或者未实现代码的代码,即用桩函数代替原函数。打桩测试由 gtest 里的 gmock 来实现。
2、GTest 安装
# 下载 git clone https://github.com/google/googletest.git # 安装 cd googletest cmake CMakeLists.txt make sudo make install
源码文件中的 lib 库,包含 gtest 库和 gmock 库。
libgtest.a libgtest_main.a libgmock.a libgmock_main.a
当测试代码有 main 函数,使用不带 main 的静态库,否则使用带 main 的静态库。
# 无 main 函数 g++ sample.cc sample_unittest.cc -o sample -lgtest -lgtest_main -lpthread # 有 main 函数 g++ gmock_output_test_.cc -o output -lgtest -lgmock -lpthread
若需要编写 main 函数,关键在于添加两个地方
int main(int argc, char **argv) { // 1.定义 main 函数:初始化 gtest ::testing::InitGoogleTest(&argc, argv); // 2.定义 main 函数:开启全部测试 return RUN_ALL_TESTS(); }
3、GTest 原理
GTest 测试底层原理
- 创建单元测试类,单例模式实现,包含
vector<TestSuite*>
- 根据测试套件名,生成一个
TestSuite
类实例,包含vector<TestInfo*>;
- 根据测试案例名,生成一个
Test_info
类实例,继承父类testing::Test
- 将测试案例实例注册到测试套件中
vector<TestSuite*>
,测试套件类实例调用run
方法执行测试
类的组织层次
// 单元测试,单例模式实现 class Impl { // 存储测试套件类实例 vector<TestSuite*>; } // 测试套件 class TestSuite { // 存储测试案例类实例 vector<TestInfo*>; // 执行测试套件中的测试案例 run(); } // 测试案例 class TestInfo { // 执行测试 TestBody(); };
4、断言
使用测试断言,通过断言其行为来测试类和函数。ASSERT_*
失败时会生成致命错误,并中止当前功能;EXPECT_*
失败时生成非致命错误,不会中止当前功能。通常选用EXPECT_*
。
所有断言宏都支持输出流,经流输出的信息自动转换为utf-8
,可利用这一特性输出详细错误信息
EXPECT_TRUE(my_condition) << "My condition is not true";
更多断言的使用,见官方文档:Assertions
明确指定成功或失败
当测试案例中的条件太复杂,不能使用断言,那么自己写判断语句;自己返回成功或者失败;
if (condition) { SUCCEED(); } else { FAIL(); }
布尔条件
EXPECT_TRUE(condition) ASSERT_TRUE(condition) EXPECT_FALSE(condition) ASSERT_FALSE(condition)
二元比较
// val1 = val2 EXPECT_EQ( val1 , val2 ) ASSERT_EQ( val1 , val2 ) // val1 != val2,空指针使用 nullptr EXPECT_NE( val1 , val2 ) ASSERT_NE( val1 , val2 ) // val1 <= val2 EXPECT_LT( val1 , val2 ) ASSERT_LT( val1 , val2 ) // val1 > val2 EXPECT_GT( val1 , val2 ) ASSERT_GT( val1 , val2 ) // val1 >= val2 EXPECT_GE( val1 , val2 ) ASSERT_GE( val1 , val2 )
谓词断言
EXPECT_PREDn( pred , val1, ..., valn ) \ ASSERT_PREDn( pred , val1, ..., valn ) \
例如:测试阶乘函数,参数 1 个
EXPECT_PRED1(Factorial, 1)
死亡测试
用于测试程序是否以预期的方式崩溃。
EXPECT_DEATH(func, desc);
5、GTest 使用
测试的方法
- 基本功能:验证基本逻辑是否正确
- 边界情况:验证边界值是否正确输出
- 异常情况:非法输入做出合理错误处理。判断错误的方式
- 函数返回值
- 全局变量:
linux: Errno
,windows: GetLastError
, - 异常:抛出异常
5.1、测试1:测试函数
使用 TEST
宏来定义测试案例。
#include "sample1.h" #include <limits.h> #include "gtest/gtest.h" // 使用 TEST 宏定义测试案例 // #define TEST(test_suite_name,test_name) // 测试阶乘:负数 TEST(FactorialTest, Negative) { // 断言:预期相等 EXPECT_EQ(expected, actual),后面同理 EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); } // 测试阶乘:0 TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); } // 测试阶乘:正数 TEST(FactorialTest, Positive) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); }
编译代码
g++ sample1.cc sample1_unittest.cc -o sample1 -lgtest -lgtest_main -lpthread
测试结果
[==========] Running 6 tests from 2 test suites. [----------] Global test environment set-up. [----------] 3 tests from FactorialTest [ RUN ] FactorialTest.Negative [ OK ] FactorialTest.Negative (0 ms) [ RUN ] FactorialTest.Zero [ OK ] FactorialTest.Zero (0 ms) [ RUN ] FactorialTest.Positive [ OK ] FactorialTest.Positive (0 ms) [----------] 3 tests from FactorialTest (0 ms total) ... [----------] Global test environment tear-down [==========] 6 tests from 2 test suites ran. (0 ms total) [ PASSED ] 6 tests.
5.2、测试2:测试类
#include <iostream> #include <initializer_list> #include <vector> #include <gtest/gtest.h> using namespace std; class IslandProblem { public: using Matrix = vector<vector<char>>; IslandProblem(const initializer_list<vector<char>> list) { _islands.assign(list); } int Do() { int num = 0; for (int row = 0; row < (int)_islands.size(); row++) { for (int col = 0; col < (int)_islands[row].size(); col++) { if (canUnion(row, col)) { num++; unionIsland(row, col); } } } return num; } protected: bool canUnion(int row, int col) { if (row < 0 || row >= (int)_islands.size()) return false; if (col < 0 || col >= (int)_islands[row].size()) return false; if (_islands[row][col] != 1) return false; return true; } void unionIsland(int row, int col) { _islands[row][col] = 2; if (canUnion(row-1, col)) unionIsland(row-1, col); if (canUnion(row, col-1)) unionIsland(row, col-1); if (canUnion(row+1, col)) unionIsland(row+1, col); if (canUnion(row, col+1)) unionIsland(row, col+1); } private: Matrix _islands; }; TEST(IslandProblem, logic) { IslandProblem ip1{ {1,1,1,1}, {1,0,1,1}, {0,0,0,0}, {1,0,1,0} }; EXPECT_EQ(ip1.Do(), 3); IslandProblem ip2{ {1,0,1,1}, {1,0,1,1}, {0,0,0,0}, {1,0,1,0} }; EXPECT_EQ(ip2.Do(), 4); } TEST(IslandProblem, boundary) { IslandProblem ip1{ {1,1,1,1}, {1,0,0,1}, {1,0,0,1}, {1,1,1,1} }; EXPECT_EQ(ip1.Do(), 1); IslandProblem ip2{ }; EXPECT_EQ(ip2.Do(), 0); } TEST(IslandProblem, exception) { IslandProblem ip1{ {-1,1,1,1}, {1,0,0,1}, {1,0,0,1}, {1,1,1,1} }; EXPECT_EQ(ip1.Do(), 1); }
5.3、测试3:测试夹具
用相同的数据配置来测试多个测试案例,实现测试夹具共享,而不是数据共享。
使用测试夹具的方法
- 在使用测试夹具前,定义测试夹具类,继承基类
testing::Test
,类成员可访问protected
, - 实现
SetUp()
接口:测试前调用,若要初始化变量,重定义该接口,否则跳过。 - 实现
TearDown()
接口:测试后调用,若有清理工作要做,重定义该接口,否则跳过。 - 自定义成员
使用 TEST_F
宏测试夹具
#include "sample3-inl.h" #include "gtest/gtest.h" namespace { // 使用测试夹具,必须继承基类 testing::Test class QueueTestSmpl3 : public testing::Test { protected: // 1、实现 SetUp() 接口 void SetUp() override { q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } // 2、实现 TearDown() 接口 // virtual void TearDown() { // } // 自定义辅助测试成员函数 // A helper function that some test uses. static int Double(int n) { return 2 * n; } // A helper function for testing Queue::Map(). void MapTester(const Queue<int>* q) { // Creates a new queue, where each element is twice as big as the // corresponding one in q. const Queue<int>* const new_q = q->Map(Double); // Verifies that the new queue has the same size as q. ASSERT_EQ(q->Size(), new_q->Size()); // Verifies the relationship between the elements of the two queues. for (const QueueNode<int>*n1 = q->Head(), *n2 = new_q->Head(); n1 != nullptr; n1 = n1->next(), n2 = n2->next()) { EXPECT_EQ(2 * n1->element(), n2->element()); } delete new_q; } // 自定义数据成员 Queue<int> q0_; Queue<int> q1_; Queue<int> q2_; }; // 使用 TEST_F 宏测试夹具 TEST_F(test_fixture, test_name) // 测试队列:构造函数 TEST_F(QueueTestSmpl3, DefaultConstructor) { EXPECT_EQ(0u, q0_.Size()); } // 测试队列:出队 TEST_F(QueueTestSmpl3, Dequeue) { int* n = q0_.Dequeue(); EXPECT_TRUE(n == nullptr); n = q1_.Dequeue(); ASSERT_TRUE(n != nullptr); EXPECT_EQ(1, *n); EXPECT_EQ(0u, q1_.Size()); delete n; n = q2_.Dequeue(); ASSERT_TRUE(n != nullptr); EXPECT_EQ(2, *n); EXPECT_EQ(1u, q2_.Size()); delete n; } // 测试队列:map() TEST_F(QueueTestSmpl3, Map) { MapTester(&q0_); MapTester(&q1_); MapTester(&q2_); } } // namespace
5.4、测试4:类型参数化
相同的接口,有多个实现,复用测试案例,策略模式。例如:写日志(写磁盘、写数据库、写 kafka)。
使用 TYPED_TEST
宏测试类型参数化
// 枚举测试类型:同一接口的不同实现形式类 typedef Types<Class1, Class2, class3, ...> Implementations; // 定义测试套件 TYPED_TEST_SUITE(TestFixtureSmpl, Implementations); // 使用 `TYPED_TEST`宏测试类型参数化 TYPED_TEST(TestFixtureSmpl, TestName)
5.5、测试5:事件
通过事件机制,在测试前后进行埋点处理。事件机制定义如下
class TersePrinter : public EmptyTestEventListener { private: // Called before any test activity starts. void OnTestProgramStart(const UnitTest& /* unit_test */) override {} // Called after all test activities have ended. void OnTestProgramEnd(const UnitTest& unit_test) override { fprintf(stdout, "TEST %s\n", unit_test.Passed() ? "PASSED" : "FAILED"); fflush(stdout); } // Called before a test starts. void OnTestStart(const TestInfo& test_info) override { fprintf(stdout, "*** Test %s.%s starting.\n", test_info.test_suite_name(), test_info.name()); fflush(stdout); } // Called after a failed assertion or a SUCCEED() invocation. void OnTestPartResult(const TestPartResult& test_part_result) override { fprintf(stdout, "%s in %s:%d\n%s\n", test_part_result.failed() ? "*** Failure" : "Success", test_part_result.file_name(), test_part_result.line_number(), test_part_result.summary()); fflush(stdout); } // Called after a test ends. void OnTestEnd(const TestInfo& test_info) override { fprintf(stdout, "*** Test %s.%s ending.\n", test_info.test_suite_name(), test_info.name()); fflush(stdout); } }; // class TersePrinter
例:内存泄漏
在需要检测的类中,重载 new 和 delete 操作符,再用静态成员统计两者的次数是否一致。
#include <stdio.h> #include <stdlib.h> #include "gtest/gtest.h" using ::testing::EmptyTestEventListener; using ::testing::InitGoogleTest; using ::testing::Test; using ::testing::TestEventListeners; using ::testing::TestInfo; using ::testing::TestPartResult; using ::testing::UnitTest; namespace { // 需要检测的类 class Water { public: // 类的定义 // 重载 new 和 delete 函数 void* operator new(size_t allocation_size) { allocated_++; return malloc(allocation_size); } void operator delete(void* block, size_t /* allocation_size */) { allocated_--; free(block); } static int allocated() { return allocated_; } private: // 静态成员,统计 new 和 delete 的次数,判断内存泄漏 static int allocated_; }; int Water::allocated_ = 0; // 检测内存泄漏:事件机制 class LeakChecker : public EmptyTestEventListener { private: // Called before a test starts. void OnTestStart(const TestInfo& /* test_info */) override { initially_allocated_ = Water::allocated(); } // Called after a test ends. void OnTestEnd(const TestInfo& /* test_info */) override { int difference = Water::allocated() - initially_allocated_; // 输出测试结果 EXPECT_LE(difference, 0) << "Leaked " << difference << " unit(s) of Water!"; } int initially_allocated_; }; // 开启内存泄漏检测 --check_for_leaks TEST(ListenersTest, DoesNotLeak) { Water* water = new Water; delete water; } // 未开启内存泄漏检测?通过判断指针是否为空,来判断是否有内存谢洛 TEST(ListenersTest, LeaksWater) { Water* water = new Water; EXPECT_TRUE(water != nullptr); } } // namespace int main(int argc, char** argv) { // 1、main 函数定义:初始化 InitGoogleTest InitGoogleTest(&argc, argv); bool check_for_leaks = false; if (argc > 1 && strcmp(argv[1], "--check_for_leaks") == 0) check_for_leaks = true; else printf("%s\n", "Run this program with --check_for_leaks to enable leak checking."); // 若启动命令行添加参数 --check_for_leaks,开启内存泄漏检测 if (check_for_leaks) { TestEventListeners& listeners = UnitTest::GetInstance()->listeners(); listeners.Append(new LeakChecker); } // 2、main 函数定义:开启全级测试 return RUN_ALL_TESTS(); }