GoogleTest 入门

介绍:为什么选择 GoogleTest?

GoogleTest 帮助你编写更好的 C++ 测试。

GoogleTest 是一个由 Google 测试技术团队开发的测试框架,它考虑到了 Google 的具体需求和约束。无论你是在 Linux、Windows 还是 Mac 上工作,如果你编写 C++ 代码,GoogleTest 都能帮助你。并且它支持任何类型的测试,不仅仅是单元测试。

那么,什么才是好的测试,GoogleTest 在其中扮演什么角色呢?我们认为:

  1. 测试应该是独立的可重复的。调试一个因其他测试而成功或失败的测试是很痛苦的。GoogleTest 通过在不同的对象上运行每个测试来隔离测试。当一个测试失败时,GoogleTest 允许你单独运行它以进行快速调试。
  2. 测试应该被很好地组织,并反映被测试代码的结构。GoogleTest 将相关的测试分组到测试套件中,这些套件可以共享数据和子程序。这种常见的模式很容易识别,并使测试易于维护。当人们切换项目并开始在一个新的代码库上工作时,这种一致性尤其有用。
  3. 测试应该是可移植的可重用的。Google 有很多平台无关的代码;它的测试也应该是平台无关的。GoogleTest 可以在不同的操作系统、不同的编译器、有或没有异常的情况下工作,因此 GoogleTest 测试可以与各种配置一起工作。
  4. 当测试失败时,它们应该提供尽可能多的关于问题的信息。GoogleTest 不会在第一个测试失败时停止。相反,它只会停止当前的测试并继续下一个测试。你也可以设置报告非致命错误的测试,在这些错误之后,当前测试继续进行。因此,你可以在一个运行-编辑-编译周期中检测和修复多个错误。
  5. 测试框架应该让测试编写者从繁琐的事务中解放出来,让他们专注于测试的内容。GoogleTest 会自动跟踪所有定义的测试,并且不需要用户枚举它们来运行。
  6. 测试应该是快速的。使用 GoogleTest,你可以在测试之间重用共享资源,只需为设置/拆卸支付一次成本,而无需使测试相互依赖。

由于 GoogleTest 基于流行的 xUnit 架构,如果你以前使用过 JUnit 或 PyUnit,你会感到宾至如归。如果不是,你将需要大约 10 分钟来学习基础知识并开始使用。所以,让我们开始吧!

注意命名约定

注意: 由于对术语测试测试用例测试套件 的不同定义可能存在一些混淆,因此请注意不要误解这些术语。

从历史上看,GoogleTest 开始使用术语 测试用例 来分组相关测试,而当前的出版物,包括国际软件测试资格委员会(ISTQB)的材料和各种软件质量教科书,使用术语 测试套件 来表示这一点。

相关术语测试,如 GoogleTest 中所使用,对应于 ISTQB 和其他机构的术语 测试用例

术语测试通常具有足够广泛的含义,包括 ISTQB 对测试用例的定义,因此这里没有太大的问题。但是,Google Test 中使用的术语 测试用例 具有矛盾的含义,因此令人困惑。

GoogleTest 最近开始用 测试套件 替换术语 测试用例。首选的 API 是 TestSuite。较旧的 TestCase API 正在慢慢弃用并被重构。

因此,请注意这些术语的不同定义:

含义 GoogleTest 术语 ISTQB 术语
使用特定的输入值来执行特定的程序路径并验证结果 TEST() 测试用例

基本概念

当使用 GoogleTest 时,你首先要编写断言,这些断言是检查某个条件是否为真的语句。断言的结果可以是成功非致命失败致命失败。如果发生致命失败,它会中止当前函数;否则程序会正常继续。

测试 使用断言来验证被测试代码的行为。如果一个测试崩溃或有一个失败的断言,那么它会失败;否则它会成功

一个测试套件 包含一个或多个测试。你应该将你的测试分组到反映被测试代码结构的测试套件中。当一个测试套件中的多个测试需要共享公共对象和子程序时,你可以将它们放入一个 测试装置 类中。

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

我们现在将解释如何编写一个测试程序,从单个断言级别开始,逐步构建到测试和测试套件。

断言

GoogleTest 断言是类似于函数调用的宏。你通过对类的行为进行断言来测试一个类或函数。当一个断言失败时,GoogleTest 会打印断言的源文件和行号位置,以及一条失败消息。你也可以提供一个自定义的失败消息,它会被附加到 GoogleTest 的消息中。

断言成对出现,它们测试相同的事情,但对当前函数有不同的影响。 ASSERT_* 版本在失败时生成致命失败,并 中止当前函数EXPECT_* 版本生成非致命失败,不会中止当前函数。通常,EXPECT_* 是首选的,因为它们允许在一个测试中报告多个失败。但是,如果当所讨论的断言失败时继续进行没有意义,你应该使用 ASSERT_*

由于失败的 ASSERT_* 会立即从当前函数返回,可能会跳过它之后的清理代码,因此可能会导致空间泄漏。根据泄漏的性质,可能值得修复,也可能不值得修复 - 因此如果除了断言错误之外还收到堆检查器错误,请记住这一点。

要提供自定义失败消息,只需使用 << 运算符或此类运算符的序列将其流式传输到宏中。请参阅以下示例,使用 ASSERT_EQEXPECT_EQ 宏来验证值相等性

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 对象。如果一个宽字符串(wchar_t*, TCHAR* 在 Windows 上的 UNICODE 模式下,或者 std::wstring)被流式传输到一个断言,它将在打印时被翻译成 UTF-8。

GoogleTest 提供了一系列断言,用于以各种方式验证你的代码的行为。你可以检查布尔条件,根据关系运算符比较值,验证字符串值、浮点值等等。甚至还有断言可以通过提供自定义谓词来验证更复杂的状态。有关 GoogleTest 提供的断言的完整列表,请参阅 断言参考

简单测试

要创建一个测试:

  1. 使用 TEST() 宏来定义和命名一个测试函数。这些是普通的 C++ 函数,不返回值。
  2. 在这个函数中,除了你想要包含的任何有效的 C++ 语句之外,使用各种 GoogleTest 断言来检查值。
  3. 测试的结果由断言决定;如果测试中的任何断言失败(无论是致命的还是非致命的),或者如果测试崩溃,则整个测试失败。否则,它会成功。
TEST(TestSuiteName, TestName) {
  ... test body ...
}

TEST() 参数从一般到具体。第一个 参数是测试套件的名称,第二个 参数是测试在测试套件中的名称。这两个名称必须是有效的 C++ 标识符,并且不应包含任何下划线 (_)。一个测试的 完整名称 由其包含的测试套件及其单独的名称组成。来自不同测试套件的测试可以具有相同的单独名称。

例如,让我们以一个简单的整数函数为例:

int Factorial(int n);  // Returns the factorial of n

该函数的测试套件可能如下所示:

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

GoogleTest 通过测试套件对测试结果进行分组,因此逻辑上相关的测试应该位于同一个测试套件中;换句话说,它们的 TEST() 的第一个参数应该相同。在上面的例子中,我们有两个测试,HandlesZeroInputHandlesPositiveInput,它们属于同一个测试套件 FactorialTest

在命名测试套件和测试时,你应该遵循与 命名函数和类相同的约定。

可用性:Linux, Windows, Mac。

测试夹具:为多个测试使用相同的数据配置

如果你发现自己编写的两个或多个测试操作类似的数据,你可以使用测试夹具。这允许你为几个不同的测试重用相同的对象配置。

要创建夹具:

  1. testing::Test 派生一个类。以 protected: 开始它的主体,因为我们希望从子类访问夹具成员。
  2. 在类内部,声明你计划使用的任何对象。
  3. 如有必要,编写一个默认构造函数或 SetUp() 函数来为每个测试准备对象。一个常见的错误是将 SetUp() 拼写为 Setup(),其中 u 是小写的 - 在 C++11 中使用 override 来确保你正确拼写了它。
  4. 如有必要,编写一个析构函数或 TearDown() 函数来释放你在 SetUp() 中分配的任何资源。要了解何时应该使用构造函数/析构函数,以及何时应该使用 SetUp()/TearDown(),请阅读 FAQ
  5. 如果需要,定义子程序供你的测试共享。

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

TEST_F(TestFixtureClassName, TestName) {
  ... test body ...
}

TEST() 不同,在 TEST_F() 中,第一个参数必须是测试夹具类的名称。(_F 代表 “Fixture”)。此宏未指定测试套件名称。

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

此外,你必须首先定义一个测试夹具类,然后才能在 TEST_F() 中使用它,否则你会收到编译器错误 “virtual outside class declaration”。

对于使用 TEST_F() 定义的每个测试,GoogleTest 将在运行时创建一个新的测试夹具,立即通过 SetUp() 初始化它,运行测试,通过调用 TearDown() 清理,然后删除测试夹具。请注意,同一测试套件中的不同测试具有不同的测试夹具对象,并且 GoogleTest 始终在创建下一个测试夹具之前删除一个测试夹具。GoogleTest 为多个测试重用同一个测试夹具。一个测试对夹具所做的任何更改都不会影响其他测试。

例如,让我们为名为 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;
  ...
};

首先,定义一个夹具类。按照惯例,你应该给它命名为 FooTest,其中 Foo 是被测试的类。

class QueueTest : public testing::Test {
 protected:
  QueueTest() {
     // q0_ remains empty
     q1_.Enqueue(1);
     q2_.Enqueue(2);
     q2_.Enqueue(3);
  }

  // ~QueueTest() override = default;

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

在这种情况下,我们不需要定义析构函数或 TearDown() 方法,因为编译器生成的隐式析构函数将执行所有必要的清理。

现在我们将使用 TEST_F() 和这个夹具编写测试。

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

上面同时使用了 ASSERT_*EXPECT_* 断言。经验法则是,当你希望测试在断言失败后继续显示更多错误时,使用 EXPECT_*;当失败后继续没有意义时,使用 ASSERT_*。例如,Dequeue 测试中的第二个断言是 ASSERT_NE(n, nullptr),因为我们稍后需要解引用指针 n,当 nNULL 时,这会导致段错误。

当这些测试运行时,会发生以下情况:

  1. GoogleTest 构造一个 QueueTest 对象(我们称之为 t1)。
  2. 第一个测试 (IsEmptyInitially) 在 t1 上运行。
  3. t1 被销毁。
  4. 上述步骤在另一个 QueueTest 对象上重复,这次运行 DequeueWorks 测试。

可用性:Linux, Windows, Mac。

调用测试

TEST()TEST_F() 隐式地向 GoogleTest 注册它们的测试。因此,与许多其他 C++ 测试框架不同,你无需重新列出所有已定义的测试即可运行它们。

在定义了你的测试之后,你可以使用 RUN_ALL_TESTS() 来运行它们,如果所有测试都成功,则返回 0,否则返回 1。请注意,RUN_ALL_TESTS() 运行你链接单元中的所有测试——它们可以来自不同的测试套件,甚至来自不同的源文件。

当调用 RUN_ALL_TESTS() 宏时:

如果发生致命错误,则后续步骤将被跳过。

重要提示:你不能忽略 RUN_ALL_TESTS() 的返回值,否则你会收到编译器错误。这种设计的理由是,自动化测试服务根据其退出代码而不是 stdout/stderr 输出确定测试是否已通过;因此你的 main() 函数必须返回 RUN_ALL_TESTS() 的值。

此外,你应该只调用 RUN_ALL_TESTS() 一次。多次调用它会与一些高级 GoogleTest 功能(例如,线程安全的死亡测试)冲突,因此不支持。

可用性:Linux, Windows, Mac。

编写 main() 函数

大多数用户不需要编写自己的 main 函数,而是与 gtest_main (而不是 gtest)链接,后者定义了一个合适的入口点。有关详细信息,请参阅本节末尾。本节的其余部分仅在你需要在测试运行之前执行一些自定义操作时适用,这些操作无法在夹具和测试套件的框架内表达。

如果你编写自己的 main 函数,它应该返回 RUN_ALL_TESTS() 的值。

你可以从这个样板代码开始:

#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();
}

testing::InitGoogleTest() 函数解析命令行的 GoogleTest 标志,并删除所有识别的标志。这允许用户通过各种标志控制测试程序的行为,我们将在高级指南中介绍这些标志。你必须在调用 RUN_ALL_TESTS() 之前调用此函数,否则标志将无法正确初始化。

在 Windows 上,InitGoogleTest() 也适用于宽字符串,因此它也可以在以 UNICODE 模式编译的程序中使用。

但也许你认为编写所有这些 main 函数太费劲了?我们完全同意你的看法,这就是为什么 Google Test 提供了 main() 的基本实现。如果它符合你的需求,那么只需将你的测试与 gtest_main 库链接,就可以了。

注意:ParseGUnitFlags() 已弃用,取而代之的是 InitGoogleTest()

已知限制