gtest: 入门指南

简介:为什么选择 GoogleTest?

那么,什么是一个好的测试,GoogleTest 又如何与之契合呢?我们认为:

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

由于 GoogleTest 基于流行的 xUnit 架构,如果您以前使用过 JUnit 或 PyUnit,您会感到非常熟悉。如果没有,您只需大约 10 分钟即可学习基础知识并开始使用。那么,让我们开始吧!

术语的注意事项

{: .callout .note}
注意: 由于术语 TestTest CaseTest Suite 的不同定义可能会引起一些混淆,因此请注意不要误解这些术语。

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

在 GoogleTest 中使用的相关术语 Test 对应于 ISTQB 和其他机构的术语 Test Case

术语 Test 通常具有足够广泛的含义,包括 ISTQB 对 Test Case 的定义,因此在这里问题不大。但 GoogleTest 中使用的术语 Test Case 具有矛盾的含义,因此容易引起混淆。

GoogleTest 最近开始用 Test Suite 替换术语 Test Case。首选的 API 是 TestSuite。较旧的 TestCase API 正在逐渐被弃用和重构。

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

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

基本概念

使用 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*、Windows 上 UNICODE 模式下的 TCHAR*std::wstring)流式传输到断言,它将在打印时转换为 UTF-8。

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

简单测试

要创建测试:

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

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

例如,我们来看一个简单的整数函数:

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

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

// 测试 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 按测试套件分组测试结果,因此逻辑上相关的测试应位于同一测试套件中;换句话说,它们的 TEST() 的第一个参数应相同。在上面的示例中,我们有两个测试,HandlesZeroInputHandlesPositiveInput,它们属于同一测试套件 FactorialTest

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

可用性:Linux、Windows、Mac。

测试夹具:为多个测试使用相同的数据配置 {#same-data-multiple-tests}

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

要创建夹具:

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

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

此外,您必须在使用 TEST_F() 之前定义一个测试夹具类,否则您将收到编译器错误“virtual outside class declaration”。

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

例如,让我们为一个名为 Queue 的 FIFO 队列类编写测试,该类具有以下接口:

template <typename E>  // E 是元素类型。
class Queue {
 public:
  Queue();
  void Enqueue(const E& element);
  E* Dequeue();  // 如果队列为空,则返回 NULL。
  size_t size() const;
  ...
};

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

class QueueTest : public testing::Test {
 protected:
  QueueTest() {
     // q0_ 保持为空
     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() 宏:

  • 保存所有 GoogleTest 标志的状态。

  • 为第一个测试创建一个测试夹具对象。

  • 通过 SetUp() 初始化它。

  • 在夹具对象上运行测试。

  • 通过 TearDown() 清理夹具。

  • 删除夹具。

  • 恢复所有 GoogleTest 标志的状态。

  • 对下一个测试重复上述步骤,直到所有测试运行完毕。

如果发生致命失败,后续步骤将被跳过。

{: .callout .important}

重要提示:您不得忽略 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 {

// 用于测试类 Foo 的夹具。
class FooTest : public testing::Test {
 protected:
  // 如果以下函数的主体为空,您可以删除其中任何一个或全部。

  FooTest() {
     // 您可以在此处为每个测试执行设置工作。
  }

  ~FooTest() override {
     // 您可以在此处执行不会抛出异常的清理工作。
  }

  // 如果构造函数和析构函数不足以设置和清理每个测试,您可以定义以下方法:

  void SetUp() override {
     // 此处的代码将在构造函数之后立即调用(在每个测试之前)。
  }

  void TearDown() override {
     // 此处的代码将在每个测试之后立即调用(在析构函数之前)。
  }

  // 此处声明的类成员可以由 Foo 的测试套件中的所有测试使用。
};

// 测试 Foo::Bar() 方法是否执行 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);
}

// 测试 Foo 是否执行 Xyz。
TEST_F(FooTest, DoesXyz) {
  // 练习 Foo 的 Xyz 功能。
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值