使用Googletest进行单元测试:概念篇
Xilong Yang

Googletest介绍

Googletest是Google开发的C++测试框架,可以方便我们为代码编写测试。因为这个框架是基于公共的xUnit测试框架,有其它测试框架使用经验的人会觉得非常熟悉。即使没有接触过其它测试框架,也能在很短的时间内上手。

什么样的测试才是好的测试呢?Google给出的说法为:

  1. 测试应该是独立并且可复现,Debug一个受其它测试结果影响的测试无疑是非常痛苦的。Googletest对每个测试使用独立的对象运行,这样就能把它们隔离开。并且Googletest支持单独运行一个测试,以便在单个测试出错时快速Debug。
  2. 测试应试有良好的组织形式,并且能够反应出被测试代码的结构。Googletest将有关系的测试编成测试组,同一个测试组中的测试可以共享数据和子程序。这种共同模式容易识别,并且使测试方便维护。
  3. 测试应该可移殖且可复用。Googletest是一个跨多平台框架。
  4. 测试失败时应该提供尽可能多的关于导致失败的问题的信息。Googletest在测试失败时会继续执行下一个测试,而不是直接退出。
  5. 测试框架应该将测试编写者从各种烦琐事务中解放出来,使他们能专注于测试内容。Googletest自动运行所有已定义的测试,不需要用户将它们按顺序排列。
  6. 测试应该足够快。Googletest可以复用共享的资源,代价很低。

一个命名上的坑

由于一些历史原因,Googletest使用TestCase来表示多个有关系的Test组成的组。而使用Test表示单次测试。

这就有坑了,因为一般单次测试是使用Test Case这个术语表示,通常译为测试用例。也就是说我们通常所说的测试用例其实在Googletest中叫做Test,而Googletest中的TestCase是多个测试用例组成的一个组。

为了弥补这个问题,Google方面推出了一个新的API名称TestSuite来取代TestCase。现在的语义下,Test表示测试用例,而TestSuite表示测试用例组。这样就避免了原来的岐义问题。

简单说,TestSuite=TestCase(不建议使用)表示测试组,Test表示单个测试。

基础概念

* 文中加粗部分表示术语。

终于进入正题了,Googletest使用断言(Assertion)来进行测试。所谓断言是指一些陈述,用以检查指定条件是否为真。一个断言的结果可能是成功非致命失败致命失败三种,将出现致命失败时,结束当前函数,其它情况则继续运行。

测试(Test)使用一些断言来验证代码的行为。如果测试崩溃或包含失败的断言,则测试失败。否则测试成功

测试组(TestSuite)包含一个或多个测试。你需要将测试组合成可以反映代码结构的测试组。当一个测试组中的多个测试需要共享资源或子程序时,你可以将它们加入一个测试固定类(test fixture class)中。

一个测试程序可以包含多个测试组。

了解这些概念后,我们就可以从断言开始建立我们的测试和测试组了。

断言

Googletest的断言是一些可调用宏。我们使用一系列关于类或函数的行为的断言去测试它们。当一个断言失败时,会显示出源文件与失败发生的行,并且附上错误信息。你可以自己定义错误信息,它们会在Googletest错误信息的后面显示。

断言有两种不同的前缀,表示两类断言。以ASSERT_开头的断言失败时产生致命失败,它们退出当前测试函数。以EXPECT_开头的断言则产生非致命失败,它会显示出错误信息,并接着执行下一条断言。通常我们使用EXPECT_*版本的断言,因为这可以使我们获得更多的错误信息。

ASSERT断言中断当前函数时,可能会跳过内存清理相关的代码,这将导致内存泄露。内存泄露不一定会导致危害性后果,但发生额外的堆检查(heap checker)错误时,就要往这方面留意一下了。

可以通过<<运算符简单地将自定义错误信息附在错误消息的末尾,如:

1
2
3
4
5
ASSERT_EQ(x.site(), 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接受的内容都可以被该输出流接受。Googletest提供了非常多样的断言,详见官方文档

简单的测试

使用测试分三步走:请客、斩首、收下当狗

  1. 使用TEST()宏来定义一个测试函数,这些函数是原生的C++函数,没有返回值。
  2. 在这些函数中可以使用任何合法的C++语句,使用Googletest提供的断言来进行检查。
  3. 测试结果取决于断言的结果,如果存在失败的断言或测试函数崩溃,则整个测试的结果为失败,否则为成功。
1
2
3
4
TEST(TestSuiteName, TestName) {
// test body
// ......
}

TEST()宏的参数按照从普通到特殊的顺序给出,先是测试组名,再是测试用例名。这些名称都必须是合法的C++标识符,并且不能包括_。一个测试用例的全名取决于它所在的组名和它自己的名称。不同测试组中的用例可以拥有相同的名称。

例如,对一个简单的函数:

1
int Factorial(int n); //返回n的阶乘

可以使用如下测试组:

1
2
3
4
5
6
7
8
9
10
11
12
// 对0的测试
TEST(FactorialTest, HandlesZeroInput) {
EXPECT_EQ(Factorial(0), 1);
}

// 对正整数的测试
TEST(FactorialTest, HandlesPositiveInput) {
EXPECT_EQ(Factorial(1), 1);
EXPECT_EQ(Factorial(2), 2);
EXPECT_EQ(Factorial(3), 6);
EXPECT_EQ(Factorial(8), 40320);
}

Googletest使用测试组来组测试分组,因此你应该将逻辑上相关的测试放在一个组中。

测试固定:为多个测试使用同样的数据配置

当你发现你正为多个测试编写相同的测试数据时,就可以使用测试固定类来复用这些相同的数据。

创建测试固定类的方法:

  1. 创建一个继承::testing::Test类的类。并将该类内部访问标签设为protected:,因为我们要从子类访问类型的信息。
  2. 在这个类中创建你想要重复使用的对象。
  3. 必要时,可以使用默认构造函数或SetUp()函数来构建这些对象。
  4. 必要时,使用析构函数或TearDown()函数来释放这些对象。
  5. 需要时可以为测试定义共享的子程序。

当使用固定类时,用TEST_F()替代TEST()以访问共享的数据与子程序:

1
2
3
4
TEST_F(TestFixtureName, TestName) {
// test body
// ......
}

TEST()相似,第一个参数表示测试组名,第二个参数表示测试名。但对于TEST_F(),第一个参数必须是测试固定类的名称。

C++的宏系统无法实现用一个宏定义同时支持两种测试类型,如果使用了错误的宏会导致编译错误。

每个测试将会使用一个新的测试固定类对象,在测试开始时构建,测试结束时析构。也就是说同一个测试组中的不同测试使用的测试固定类对象是不同的,在下一个测试开始前会重新创建一个测试固定类对象。因此在一个测试中对测试固定类对象的任何改变都不会影响下一个测试。

以下面这个类名为Queue的FIFO队列为例:

1
2
3
4
5
6
7
8
9
template <typename E>  // E是元素类型
class Queue {
public:
Queue();
void Enqueue(const E& element);
E* Dequeue();
site_t size() const;
...
};

首先我们来创建一个测试固定类,为它命名为XxxTest,其中Xxx是被测类的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
class QueueTest : public ::testing::Test {
protected:
void SetUp() override {
q1_.Enqueue(1);
q2_.Enqueue(2);
q3_.Enqueue(3);
}
// void TearDown() override {}

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

本例中并不需要手动清理数据,因此不必定义TearDown()

现在我们可以使用TEST_F()和这个固定类来编写测试了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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_*,因为空指针错误
// 会使下面对指针操作的检查失去意义。
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;
}

运行这些测试时,会进行如下操作:

  1. Googletest构建一个QueueTest类型的对象,这里不妨称之为t1
  2. t1.SetUp()初始化t1
  3. 使用t1运行第一个测试(IsEmptyInitially)。
  4. t1.TearDown(),测试结束后清理t1
  5. t1被析构掉。
  6. 对接下来的每个测试重复上述步骤。

启动测试

TEST()TEST_F()会隐式地在Googletest中注册它们定义的测试。因此,不同于许多其它C++测试框架,你无需手动为你定义的测试指定运行顺序。

你可以使用RUN_ALL_TESTS()宏来运行你的测试,当所有测试都成功时,它返回0,否则返回1。注意,RUN_ALL_TESTS()会运行程序链接的所有测试,这些测试可以来自不同的测试组甚至不同的源文件。

当运行RUN_ALL_TESTS()时:

  • 保存所有Googletest flag的状态。
  • 为第一个测试创建测试固定对象。
  • 使用SetUp()初始化这个对象。
  • 使用这个对象来运行测试。
  • 使用TearDown()清理这个对象。
  • 删除这个对象。
  • 恢复所有Googletest flag的状态。
  • 为每个测试重复上述步骤。

当发生致命失败时,后续步骤不会执行。

!你不能忽视RUN_ALL_TESTS()的返回值,否则将引起编译错误。main()函数必须返回RUN_ALL_TESTS()的返回值。

RUN_ALL_TESTS()只能调用一次。多次调用它与一些高级特性(如线程安全)冲突。因此不支持多次调用。

关于main()函数

大部分用户不需要自己编写main函数,可以通过链接gtest_main来引入一个普适的main函数。这里也不过多介绍。

已知限制

尽管Googletest设计为支持线程安全的。但只有在系统使用pthreads库时可以实现线程安全。因此目前在其它系统(如Windows)上多线程地使用Googletests是不安全的。