文章目录
什么是 GoogleTest ?
简介
GoogleTest
GoogleTest(简称 GTest) 是 Google 开源的一个跨平台的(Liunx、Mac OS X、Windows等)的 C++ 单元测试框架,可以帮助程序员测试 C++ 程序的结果预期。不仅如此,它还提供了丰富的断言、致命和非致命判断、参数化、”死亡测试”等等。
GoogleTest 官网:https://google.github.io/googletest/
GitHub 仓库:https://github.com/google/googletest
单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。至于单元的大小或范围,并没有一个明确的标准,单元可以是一个函数、方法、类、功能模块或者子系统。
单元测试通常和白盒测试联系到一起,如果单从概念上来讲两者是有区别的,不过我们通常所说的单元测试和白盒测试都认为是和代码有关系的,所以在某些语境下也通常认为这两者是同一个东西。还有一种理解方式,单元测试和白盒测试就是对开发人员所编写的代码进行测试。
优势
- 测试是独立的和可重复的。GoogleTest 使每个测试用例运行在不同的对象上,从而使测试之间相互独立。当测试失败时,GoogleTest 允许单独运行它以进行快速调试。
- 测试有良好的组织,可以反映被测试代码的结构。 GoogleTest 将相关测试划分到一个测试组内,组内的测试能共享数据,使测试易于维护。
- 测试是可移植的和可重复使用的。 与平台无关的代码,其测试代码也应该和平台无关,GoogleTest 能在不同的操作系统下工作,并且支持不同的编译器。
- 当测试用例执行失败时,提供尽可能多的有效信息,以便定位问题。 GoogleTest 不会在第一次测试失败时停止。相反,它只会停止当前测试并继续下一个测试。还可以设置报告非致命故障的测试,然后继续当前测试。因此,您可以在单个运行-编辑-编译周期中检测和修复多个错误。
- 测试框架应该将测试编写者从琐事中解放出来,让他们专注于测试内容。 GoogleTest 自动跟踪所有定义的测试,不需要用户为了运行它们而进行枚举。
- 测试高效、快速。GoogleTest 能在测试用例之间复用测试资源,只需支付一次设置/拆分成本,并且不会使测试相互依赖,这样的机制使单元测试更加高效。
环境搭建
安装 GoogleTest
Bazel
首先创建一个工作区目录:
$ mkdir my_workspace &&cd my_workspace
接着在工作区的根目录中创建一个 WORKSPACE 文件,并在其中引入外部依赖 GoogleTest,此时 Bazel 会自动去 Github 上拉取文件:
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name ="com_google_googletest",
urls =["https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip"],
strip_prefix ="googletest-609281088cfefc76f9d0ce82e1ff6c30cc3591e5",
)
接着选取一个目录作为包目录,在该目录下进行代码的编写,例如一个 hello_test.cc 文件:
#include<gtest/gtest.h>// Demonstrate some basic assertions.TEST(HelloTest, BasicAssertions){// Expect two strings not to be equal.EXPECT_STRNE("hello","world");// Expect equality.EXPECT_EQ(7*6,42);}
在同目录下的 BUILD 文件中说明目标编译的规则:
cc_test(
name ="hello_test",
size ="small",
srcs =["hello_test.cc"],
deps =["@com_google_googletest//:gtest_main"],
)
此时执行以下命令即可构建并运行测试程序:
bazel test --test_output=all //:hello_test
Cmake
首先创建一个目录:
$ mkdir my_project &&cd my_project
接着创建 CMakeLists.txt 文件,并声明对 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,我们创建一个 hello_test.cc 文件:
#include<gtest/gtest.h>// Demonstrate some basic assertions.TEST(HelloTest, BasicAssertions){// Expect two strings not to be equal.EXPECT_STRNE("hello","world");// Expect equality.EXPECT_EQ(7*6,42);}
然后在 CMakeLists.txt 的末尾加上 hello_test.cc 的构建规则:
enable_testing()
add_executable(
hello_test
hello_test.cc
)
target_link_libraries(
hello_test
gtest_main
)
include(GoogleTest)
gtest_discover_tests(hello_test)
运行下面的命令构建并允许测试程序:
cmake -S . -B build
cmake --build build
cd build && ctest
安装示例项目
从 GoogleTest 的 GitHub 中下载官方提供的示例项目:
git clone https://github.com/google/googletest.git
示例项目位于
googletest/googletest/samples/
目录,示例内容可参考官网的说明:samples
GoogleTest 实战
断言
断言的概念
GoogleTest 中,是通过断言(Assertion)来判断代码实现的功能是否符合预期。断言的结果分为成功(success)、非致命错误(non-fatal failture)和致命错误(fatal failture)。
- 成功:断言成功,程序的行为符合预期,程序允许继续向下运行。可以在代码中调用
SUCCEED()
来表示这个结果。 - 非致命错误:断言失败,但是程序没有直接中止,而是继续向下运行。可以在代码中调用
FAIL()
来表示这个结果。 - 致命错误:中止当前函数或者程序崩溃。可以在代码中调用
ADD_FAILURE()
来表示这个结果。
GoogleTest 的断言是类似于函数调用的宏。通过对其行为进行断言来测试一个类或函数。当断言失败时,GoogleTest 会打印断言的源文件和行号位置以及失败消息。还可以提供自定义失败消息,该消息将附加到 GoogleTest 的消息中。
EXPECT 与 ASSERT
宏的格式有两种:
EXPECT_*
:在失败时会产生非致命故障,不会中止当前功能。ASSERT_*
:在失败时会产生致命错误,并中止当前函数,同一用例后面的语句将不再执行。
通常
EXPECT_*
是首选,因为它们允许在测试中报告多个。但是如果在某条件不成立,程序就无法运行时,就应该使用
ASSERT_*
。
另一方面,因为
ASSERT_*
是直接从当前函数返回的,可能会导致一些内存、文件资源没有释放,因此存在内存泄漏的问题。
GoogleTest 提供了一组断言,用于以各种方式验证代码的行为。包括检查布尔条件、基于关系运算符比较值、验证字符串值、浮点值等等。甚至还有一些断言可以通过提供自定义谓词来验证更复杂的状态。有关 GoogleTest 提供的断言的完整列表,可以参考官方文档:Assertions。
自定义失败信息
如果想要提供自定义的失败信息,只需要使用流操作符 << 将这些信息流到断言宏中,例如:
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;}
任何可以流向 std::ostream 的东西都可以流向断言宏,特别是 C 语言的字符串(char)和 std::string 对象。如果一个宽字符串(wchar_t,Windows 上 UNICODE 模式下的 TCHAR*,或 std::wstring)被流向一个断言,它将在打印时被转换成 UTF-8。
功能测试
TEST
如果想要创建测试,可以使用宏函数
TEST
,它具有以下特点:
TEST
是一个没有返回值的宏函数。- 我们可以在该函数中使用断言来检测代码是否有效,测试的结果由断言决定。如果测试中的任何断言失败(致命或非致命),或者如果测试崩溃,则整个测试失败。否则,它会成功。
函数定义如下:
TEST(TestSuiteName, TestName){... test body ...}
TestSuiteName 对应测试用例集名称,TestName 是归属的测试用例名称。这两个名称都必须是有效的 C++ 标识符,并且它们不应包含任何下划线。测试的全名由其包含的测试用例集及其测试名称组成。来自不同测试用例集的测试可以具有相同的名称。
这里以官方提供的 Sample1 中的阶乘函数为例:
sample1.cc 中的阶乘函数定义如下:
intFactorial(int n){int result =1;for(int i =1; i <= n; i++){
result *= i;}return result;}
sample1_unittest.cc 中即为测试代码,这里为了能够更好的组织测试用例,将数据根据正负数划分为三个测试用例集,每一个用例集中都是相同类型的测试用例。
// 测试负数TEST(FactorialTest, Negative){EXPECT_EQ(1,Factorial(-5));EXPECT_EQ(1,Factorial(-1));EXPECT_GT(Factorial(-10),0);}// 测试0TEST(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));}
当我们执行测试时,输出如下:
[==========] Running 6 tests from 2test cases.
[----------] 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 (2 ms total)
这就表示我们的代码通过了所有的测试用例。
TEST_F
如果发现自己编写了两个或多个对相似数据进行操作的测试,可以使用 test fixture 来为多个测试重用这些相同的配置。
fixture,其语义是固定的设施,而 test fixture 在 GoogleTest 中的作用就是为每个 TEST 都执行一些同样的操作。
其对应的宏函数是
TEST_F
,函数定义如下:
TEST_F(TestFixtureName, TestName){... test body ...}
TestFixtureName 对应一个 test fixture 类的名称。因此我们需要自己去定义一个这样的类,并让它继承
testing::Test
类,然后根据我们的需要实现下面这两个虚函数:
virtual void SetUp()
:类似于构造函数,在TEST_F
之前运行;virtual void TearDown()
:类似于析构函数,在TEST_F
之后运行。
此外
testing::Test
还提供了两个 static 函数:
static void SetUpTestSuite()
:在第一个TEST
之前运行static void TearDownTestSuite()
:在最后一个TEST
之后运行
除了这两种,还有一种全局事件,即继承
testing::Environment
,并实现下面两个虚函数:
virtual void SetUp()
:在所有用例之前运行;virtual void TearDown()
:在所有用例之后运行。
这里以官方提供的 Sample3 中实现的 Queue 为例,其实现如下:
#ifndefGOOGLETEST_SAMPLES_SAMPLE3_INL_H_#defineGOOGLETEST_SAMPLES_SAMPLE3_INL_H_#include<stddef.h>template<typenameE>// E is the element typeclassQueue;template<typenameE>// E is the element typeclassQueueNode{friendclassQueue<E>;public:const E&element()const{return element_;}
QueueNode*next(){return next_;}const QueueNode*next()const{return next_;}private:explicitQueueNode(const E& an_element):element_(an_element),next_(nullptr){}const QueueNode&operator=(const QueueNode&);QueueNode(const QueueNode&);
E element_;
QueueNode* next_;};template<typenameE>// E is the element type.classQueue{public:Queue():head_(nullptr),last_(nullptr),size_(0){}~Queue(){Clear();}voidClear(){if(size_ >0){// 1. Deletes every node.
QueueNode<E>* node = head_;
QueueNode<E>* next = node->next();for(;;){delete node;
node = next;if(node ==nullptr)break;
next = node->next();}
head_ = last_ =nullptr;
size_ =0;}}
size_t Size()const{return size_;}
QueueNode<E>*Head(){return head_;}const QueueNode<E>*Head()const{return head_;}
QueueNode<E>*Last(){return last_;}const QueueNode<E>*Last()const{return last_;}voidEnqueue(const E& element){
QueueNode<E>* new_node =newQueueNode<E>(element);if(size_ ==0){
head_ = last_ = new_node;
size_ =1;}else{
last_->next_ = new_node;
last_ = new_node;
size_++;}}
E*Dequeue(){if(size_ ==0){returnnullptr;}const QueueNode<E>*const old_head = head_;
head_ = head_->next_;
size_--;if(size_ ==0){
last_ =nullptr;}
E* element =newE(old_head->element());delete old_head;return element;}template<typenameF>
Queue*Map(F function)const{
Queue* new_queue =newQueue();for(const QueueNode<E>* node = head_; node !=nullptr;
node = node->next_){
new_queue->Enqueue(function(node->element()));}return new_queue;}private:
QueueNode<E>* head_;// The first node of the queue.
QueueNode<E>* last_;// The last node of the queue.
size_t size_;// The number of elements in the queue.Queue(const Queue&);const Queue&operator=(const Queue&);};#endif// GOOGLETEST_SAMPLES_SAMPLE3_INL_H_
接着看看测试用例是如何编写的,首先声明了一个 test fixture 类,在这个类中实现了一些测试时用到的辅助函数,以及使用
SetUp
预置了一些测试数据。(除了有特殊需求,则不需要实现
TearDown
,因为析构函数已经帮我们释放了资源)
classQueueTestSmpl3:public testing::Test{protected:voidSetUp()override{
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);}// 一个辅助函数staticintDouble(int n){return2* n;}// 测试 Queue::Map() 时用到的辅助函数voidMapTester(const Queue<int>* q){const Queue<int>*const new_q = q->Map(Double);ASSERT_EQ(q->Size(), new_q->Size());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(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
这里以 DefaultConstructor 为例,来分析一下它的执行流程:
- QueueTestSmpl3 调用构造函数,构造对象。
- QueueTestSmpl3 对象调用
SetUp
函数初始化测试配置。 - DefaultConstructor 开始执行并结束测试。
- QueueTestSmpl3 对象调用隐式生成的
TearDown
进行清理。 - QueueTestSmpl3 调用析构函数,析构对象。
运行测试
调用测试
与其他 C++ 框架不同,
TEST
和
TEST_F
会隐式向 GoogleTest 注册这些测试函数,因此我们不需要为了运行这些它们而进行枚举。
定义完测试后,你可以用
RUN_ALL_TESTS
来运行它们,此时会运行所有的测试,如果全部成功则返回 0,反之则返回 1。其执行流程如下:
- 保存所有 GoogleTest 标志的状态。
- 为第一个测试创建一个 test fixture 对象。
- 通过
SetUp
初始化 test fixture 对象。 - 在 test fixture 对象上进行测试。
- 通过
TearDown
清理 test fixture。 - 删除 test fixture。
- 恢复所有 GoogleTest 标志的状态。
- 对下一个测试重复上述步骤,直到所有测试都运行完毕。
如果发生致命故障,则将跳过后续步骤。
编写 main 函数
用户不需要编写自己的
main
函数,编译器会自动将它链接到
gtest_main
。如果用户有特殊需求,需要编写一个
main
函数,则需要在返回时调用
RUN_ALL_TESTS()
来运行所有测试,例如:
intmain(int argc,char**argv){printf("Running main() from %s\n",__FILE__);
testing::InitGoogleTest(&argc, argv);returnRUN_ALL_TESTS();}
testing::InitGoogleTest
函数会解析 GoogleTest 标志的命令行,并删除所有识别的标志。这允许用户通过各种标志控制测试程序的行为。您必须在调用
RUN_ALL_TESTS
之前调用此函数 ,否则标志将无法正确初始化。
版权归原作者 凌桓丶 所有, 如有侵权,请联系我们删除。