Google C++ Test Framework使用

亢建木
2023-12-01

Google C++ Test Framework使用

简介

Google C++ Testing Framework(以下简称gtest),是一套为C++程序编写测试用例的测试框架。由于gtest跨平台的,所以不论你使用Linux,还是Windows,又或者是其他的操作系统,它都能很好的帮助你编写测试用例。

安装

  • 下载

    下载地址:https://github.com/google/googletest/releases

  • 部署

    由于google是以源码的形式发布gtest的,为了用gtest写测试程序,我们首先需要手动编译gtest生成库,然后在测试程序中使用gtest库

    • 解压缩gtest

      解压缩后会发现,有很多文件夹和文件,下面介绍几个重要的文件:

      • include

        包含gtest框架的所有头文件

      • src

        包含gtest框架的所有源文件

      • samples

        测试用的例子

      • make

        为GNU make编译gtest库准备

      • codegear

        为Borland C++ Builder编译gtest库准备

      • msvc

        为visual studio编译gtest库准备

      • xcode

        为Mac Xcode编译gtest库准备

      • CMakeLists.txt

        为cmake准备

    • 编译

      • make 编译

        进入make目录,执行make命令会生成gtest的静态库gtest_main.a,与此同时,顺便关联了samples中的第一个测试程序sample1,并且生成测试程序sample_unittest,可以执行这个程序,看下结果。

      • cmake编译
    • 运行实例

基本概念

断言是用来检测条件是否为true的语句,并且你可以通过写断言来使用gtest,一个断言的结果可能是成功,非致命的失败以及致命的失败。如果一个致命的失败出现,它将中断当前函数;否则程序正常运行。

测试用断言验证被测试代码的行为。如果一个测试崩溃或者有一个失败的断言,然后它就失败了,否则它就成功了。

一个测试用例包含一个或多个测试。你应该根据被测试的代码的结构来将测试组织成不同的测试用例。当测试用例中的大量的测试需要共享公共对象和子程序时,你可以将这些被共享的东西放进一个测试装备类(test fixture class)中。

一个测试程序中可能包含大量的测试用例。现在我们将要解释如何写一个测试程序,从单个的断言水平和构建测试和测试用例开始。

断言

gtest 断言是类似函数调用的宏。你可以通过为一个类或一个函数的行为做出断言的方式来做测试。当断言失败的时候,gtest会将会将断言的源文件和行号位置连同一些失败的消息一起打印出来。你也可以提供一些自定义的失败消息追加在gtest的消息后面。

断言都是成对的(ASSERT_*,EXPECT_*),但是它们测试同样的程序会有不同的影响。使用ASSERT_*版本时,当断言失败时,将会生成致命失败,并且中断当前函数,然而如果用EXPECT_*版本时,将会出现非致命失败,并且不会中断当前函数。通常大家更喜欢用EXPECT_*版本,因为它们允许在一个测试中超过一个失败被报告(出现失败不会中断)。然而,如果当断言失败后,继续执行没有任何意义的话,要使用ASSERT_*版本。

因为一个失败的ASSERT_*会立即从当前函数中返回,可能会跳过这个断言后面的清理代码,它可能导致空间泄漏。根据这个泄漏的特性,它值得或者不得修改-因此牢记这一点你可能得到一个堆检查程序错误除了断言错误。

为了提供一个自定义失败信息,可以将消息用‘ << ’操作符流入宏,例如:

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;
}

任何可以被流进ostream中的都可以被流进断言宏中尤其是c字符串和string对象。如果宽字符串流进一个宏,打印的时候将会被转换成UTF-8

基本断言

这些断言只是做基本的true/false 条件测试。

致命断言非致命断言验证
ASSERT_TRUE(condition);EXPECT_TRUE(condition);condition is true
ASSERT_FALSE(condition);EXPECT_FALSE(condition);condition is false

当失败的时候,ASSERT_*产生致命的失败并且从当前函数中返回;然而EXPECT_*产生一个非致命的失败,允许函数继续运行。在这两种情况下,一个断言失败意味着包含它的测试失败。

二进制比较

下面断言是用来比较两个值

致命断言非致命断言验证
ASSERT_EQ(expected,actual);EXPECT_EQ(expected,actual);expected == actual
ASSERT_NE(val1,val2);EXPECT_NE(val1,val2);val1 != val2
ASSERT_LT(val1,val2);EXPECT_LT(val1,val2);val1 < val2
ASSERT_LE(val1,val2);EXPECT_LE(val1,val2);val1 <= val2
ASSERT_GT(val1,val2);EXPECT_GT(val1,val2);val1 > val2
ASSERT_GE(val1,val2);EXPECT_GE(val1,val2);val1 >= val2

一个失败发生时,gtest将会打印val1和val2。在ASSERT_*和EXPECT_*(和所以其他我们稍后将要介绍的相似的断言)中,你应该把你想要测试的表达式放到actual的位置,把你对表达式结果的期待值放到expected的位置。并且gtest的失败消息将会根据本公约进行优化。

值参数必须能够被断言的比较操作比较,否则的话将会出现编译错误。过去为了像输出流中输出信息,我们常常需要参数支持‘ << ’操作符,但是它不在必须自从V1.6.(如果 支持<<,当断言失败的时候,它将会被调用并且打印出arguments)

这些宏的参数也可以是用户自定义类型。但是仅仅是你为定义的类型定义了相应的比较操作符(eg: ==, < ,etc)。如果相应的操作符被定义,推荐使用ASSERT_*()宏,因为他们将会不仅会打印出比较结果,也会打印出这两个操作数。

参数总是被准确的评估一次。因此,参数有副作用是没有关系的。然而和任何普通的c/c++函数一样,参数评估顺序是未定义的。并且你的代码不应该依赖任何的特定参数评估顺序。

ASSERT_EQ()判断指针相等仅仅是针对指针的。例如判断两个C字符串,测试的是这两个字符串是否在同样的内存位置,而不是他们是否有相同的内容(可以看字符串比较)。

这些宏对窄字符和宽字符都起作用。

字符串比较

这些断言是用来比较两个C字符串。如果想要要比较两个string对象看二进制比较。

致命断言非致命断言验证
ASSERT_STREQ(expected_str,actual_str);EXPECT_STREQ(expected_str,actual_str);两个c字符串有相同的内容
ASSERT_STRNE(str1,str2);EXPECT_STRNE(str1,str2);两个c字符串有不同的内容
ASSERT_STRCASEEQ(expected_str,actual_str);EXPECT_STRCASEEQ(expected_str,actual_str);两个c字符串有相同的内容,忽略大小写。
ASSERT_STRCASENE(str1,str2);EXPECT_STRCASENE(str1,str2);两个c字符串有不同的内容,忽略大小写。

注意在一个断言中有“CASE”代表忽略大小写。

这些宏都可以接受宽字符串(wchar_t *)。如果比较两个宽字符串失败,这两个字符串将会以UTF-8的窄字符串打印。

空指针和空字符串是被认为不同的。

简单测试

为了创建一个测试:
1. 用TEST()宏定义并且命名一个测试函数,这是普通的C++函数并且没有返回值。
2. 在这个函数中,可以写任何你想包括的有效C++代码,以及用各种gtest宏去检测值。
3. 这个测试的结构由内部的断言决定;如果有任何断言失败(不论是致命的还是非致命的),或者测试崩溃,这整个测试失败,否则就成功。

TEST(test_case_name, test_name) {
    ... test body ...
    ...在这里你可以写断言或者C++代码...
}

TEST()的参数是从一般到具体。第一个参数是这个测试用例的名字,第二个参数是测试的名字(包含在测试用例中)。两个参数的名字必须是有效C++标识符,并且他们不应该包含下划线(_)。一个测试的名字由包含它的测试用例名字以及测试单个名字。不同测试用例的测试可能有相同的测试名字。
一个小例子:

// 被测函数
int Factorial(int n); // Returns the factorial of n

测试

// Tests factorial of 0.
TEST(FactorialTest, HandlesZeroInput) {
    EXPECT_EQ(1, Factorial(0));
}

// Tests factorial of positive numbers.
TEST(FactorialTest, HandlesPositiveInput) {
    EXPECT_EQ(1, Factorial(1));
    EXPECT_EQ(2, Factorial(2));
    EXPECT_EQ(6, Factorial(3));
    EXPECT_EQ(40320, Factorial(8));
}

gtest按照测试用例为测试结果分组,因此逻辑相关的测试应该在同样的测试用例中,总而言之,TEST()的第一个参数应该相同。在上面例子中,我们有两个测试,HandlesZeroInput和HandlesPositiveInput,并且属于同一个测试实例FactorialTest

测试模具:为大量的测试用同样的数据配置

如果你发现你写两个或者更多测试操作相似的数据,你可以用一个测试模具。它允许你在几个不同的测试中重用同样对象的配置

为了创建模具:
1. 从::testing::Test类中以protected:或public:的形式派生一个类。因为我们想在子类中访问模具成员.
2. 在类中,你可以声明任何你计划用的对象
3. 如果必要的话,你可以写一个构造函数或者SetUp()函数为每次测试初始化对象。一个经常出现的问题是把SetUp()中的’U’写错成’u’,希望你不要写错
4. 如果必要的话,你可以写一个析构函数或者TearDown()函数为释放任何你在SetUp()中分配的资源。
5. 如果需要的话,你可以定义子程序为所有的测试共享。

当用测试模具时,用TEST_F()而不是TEST(),因为它允许你访问对象和子程序在测试模具中:

TEST_F(test_case_name, test_name) {
    ... test body ...
}

类似TEST(),第一个参数是测试用例的名字,但是TEST_F()必须用测试模具的类名来做测试用例的名字。

不幸的是,c++宏不允许我们创建一个可以处理两种测试类型的宏。用错误的宏会导致编译错误。

你必须首先定义测试模具类,然后才能在TEST_T()中使用,否则将会出现编译错误“virtual outside class declaration”。

对于每一个被定义成TEST_F()的测试,gtest将会:
1. 在运行时将会创建一个新的测试模具
2. 通过SetUp()立即出事化测试模具。
3. 运行测试
4. 通过TearDown()做清理
5. 删除测试模具
注意在同一个测试用例中的不同的测试将会有不同的测试模具对象,并且gtest总会在创建下一个测试模具前删除测试模具。gtest不会为大量的测试使用同样的测试模具对象。一个测试制造的任何改变都不会影响其他的测试。

作为一个例子,我们为一个叫Queue的FIFO的队列类写测试

template <typename E> // E is the element type.
class Queue {
    public:
        Queue();
        void Enqueue(const E& element);
        E* Dequeue(); // Returns NULL if the queue is empty.
        size_t size() const;
    ...
};

首先,定义一个测试模具类,按照约定,当Foo类被测试你应该给测试模具类起名为FooTest

class QueueTest : public ::testing::Test {
    protected:
        virtual void SetUp() {
        q1_.Enqueue(1);
        q2_.Enqueue(2);
        q2_.Enqueue(3);
    }

    // virtual void TearDown() {}

    Queue<int> q0_;
    Queue<int> q1_;
    Queue<int> q2_;
};

在这个例子中,TearDown()是不被需要,因为我们不需要为任何测试做清理。

现在我们将用TEST_F()和这个模具为tests写测试。

TEST_F(QueueTest, IsEmptyInitially) {
    EXPECT_EQ(0, q0_.size());
}

TEST_F(QueueTest, DequeueWorks) {
    int* n = q0_.Dequeue();
    EXPECT_EQ(NULL, n);

    n = q1_.Dequeue();
    ASSERT_TRUE(n != NULL);
    EXPECT_EQ(1, *n);
    EXPECT_EQ(0, q1_.size());
    delete n;

    n = q2_.Dequeue();
    ASSERT_TRUE(n != NULL);
    EXPECT_EQ(2, *n);
    EXPECT_EQ(1, q2_.size());
    delete n;
}

上面用了ASSERT_*和EXPECT_*断言。按照经验,在遇到断言失败后,你想要这次测试发现更多的错误的时候用EXPECT_*,失败后就没有任何意义的话就用ASSERT_*。例如,在Dequeue测试中第二个断言是ASSERT_TRUE(n != NULL),因为当n为空时,就爱年光辉导致一个段错误,并且我们需要弃用这个指针n。

当测试运行的时候,会发生下列的事情:
1. gtest 构造一个QueueTest对象(让我们叫它t1)
2. t1.SetUp()初始化t1
3. 第一个测试运行
4. 在测试完成后t1.TearDown()清理
5. t1析构
6. 对另一个QueueTest对象重复上面的步骤

调用测试

TEST()和TEST_F()用gtest隐式注册测试。因此,不像其他任何的C++测试框架,当你运行测试的时候,你不需要重新列出所有你定义的测试。

在定义测试后,你可以通过RUN_ALL_TESTS()运行它们,并且如果所有的测试成功的话返回0,否则返回1。注意RUN_ALL_TESTS()会运行你的连接单元内的所有测试–它们可能属于不同的测试用例,或者甚至不同的源文件。

当调用的时候,RUN_ALL_TESTS()宏会做一下事情:
1. 保存gtest所有的标志状态
2. 为第一个测试创建测试模具
3. 通过SetUp()初始化
4. 根据测试模具运行测试
5. 通过TearDown()清理模具
6. 删除模具
7. 恢复gtest的所有标志状态
8. 为下一个测试重复以上步骤,直到所有的测试运行完

除此之外,在第2步如果测试模具构造出现致命失败,将跳过3-5步,相似地,如果步骤3出现致命失败,步骤4会被跳过

重要:你一定不要忽略RUN_ALL_TESTS()的返回值,否则gcc将会出现编译错误。这个设计的基本原理是自动化测试服务决定测试是否通过是根据其退出代码,不是基于stdout/stderr输出;因此你的main()函数必须返回RUN_ALL_TESTS()的返回值。
并且,你应该仅仅调用RUN_ALL_TESTS()一次。调用多次将会和gtest的一些特性冲突,因此不支持。

写main()函数

#include "this/package/foo.h"
#include "gtest/gtest.h"

namespace {

    // The fixture for testing class Foo.
    class FooTest : public ::testing::Test {
        protected:
        // You can remove any or all of the following functions if its body
        // is empty.

        FooTest() {
            // You can do set-up work for each test here.
        }

        virtual ~FooTest() {
            // 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:

        virtual void SetUp() {
            // Code here will be called immediately after the constructor (right
            // before each test).
        }

        virtual void TearDown() {
            // Code here will be called immediately after each test (right
            // before the destructor).
        }

        // Objects declared here can be used by all tests in the test case for Foo.
    };

    // Tests that the Foo::Bar() method does Abc.
    TEST_F(FooTest, MethodBarDoesAbc) {
        const string input_filepath = "this/package/testdata/myinputfile.dat";
        const string output_filepath = "this/package/testdata/myoutputfile.dat";
        Foo f;
        EXPECT_EQ(0, f.Bar(input_filepath, output_filepath));
    }

    // Tests that Foo does Xyz.
    TEST_F(FooTest, DoesXyz) {
        // Exercises the Xyz feature of Foo.
    }

}  // namespace

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

::testing::InitGoogleTest()函数是用来为gtest标志解析命令行,并且移除所有公认的标志。这允许用户通过各种各样的标志控制测试程序的行为。你必须在调用RUN_ALL_TESTS()之前调用它。

但是或许你认为为所有的测试写main函数会太麻烦?gtest提供一个main()函数的基本实现。如果它满足你的需求,你可以将你的测试链接gtest_main库。

 类似资料: