入门

2020/2/6 单元测试code tool

# 为什么选择 Google test

googletest 可帮助您编写更好的 C++ 测试。googletest 是由测试技术团队根据 Google 的特定要求和约束开发的测试框架。无论您是在 Linux,Windows 还是 Mac 上工作,如果您编写 C++ 代码,googletest 都可以为您提供帮助。它支持任何类型的测试,而不仅仅是单元测试。

那么什么才是好的测试,以及 googletest 如何适应呢?我们相信:

  1. 测试应独立可重复。调试由于其他测试而成功或失败的测试是很痛苦的。googletest 通过在不同的对象上运行每个测试来隔离测试。如果测试失败,则 googletest 允许您单独运行它以进行快速调试。

  2. 测试应该井井有条,并能反映出测试代码的结构。googletest 将相关测试分组到可以共享数据和子例程的测试套件中。这种通用模式易于识别,并使测试易于维护。当人们切换项目并开始在新的代码库上工作时,这种一致性特别有用。

  3. 测试应该是可移植且可重复使用的。Google 有很多与平台无关的代码;其测试也应该与平台无关。googletest 可以在不同的 OS,不同的编译器上运行,无论有无例外,因此 googletest 测试可以在多种配置下运行。

  4. 如果测试失败,则应提供有关该问题的尽可能多的信息。googletest 不会在第一次测试失败时停止。相反,它只会停止当前测试并继续进行下一个测试。您还可以设置报告非致命故障的测试,然后继续当前测试。因此,您可以在一个运行-编辑-编译循环中检测并修复多个错误。

  5. 测试框架应将测试编写者从杂务劳动中解放出来,并让他们专注于测试内容。googletest 自动跟踪定义的所有测试,并且不需要用户枚举即可运行它们。

  6. 测试应该很快。使用 googletest,您可以在测试之间重用共享资源,并且只需支付一次设置/拆卸费用,而无需使测试相互依赖。

由于 googletest 基于流行的 xUnit 体系结构,因此如果您以前使用过 JUnit 或 PyUnit,就会感到宾至如归。如果没有,那么大约需要 10 分钟来学习基础知识并开始使用。所以开始吧!

# 注意命名

注意:术语“测试”,“测试用例”和“测试套件”的不同定义可能会引起混淆,因此请注意不要误解这些术语。

从历史上看,googletest 开始使用“测试用例”一词来对相关测试进行分组,而当前的出版物(包括国际软件测试资格委员会(ISTQB (opens new window))资料和有关软件质量的各种教科书)都使用了“测试套件 (opens new window)”一词。

googletest 中使用的相关术语“测试”对应于 ISTQB “测试用例 (opens new window)”和其他术语。

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

googletest 最近开始用 Test Suite 取代术语 Test Case。首选的 API 是 TestSuite。较旧的 TestCase API 逐渐被弃用并重构。

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

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

# 基本概念

使用 googletest 时,您首先要编写 assertions(断言),断言是检查条件是否为真的语句。断言的结果可能是成功非致命失败致命失败。如果发生致命故障,它将中止当前功能。否则程序将继续正常运行。

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

一个测试套件包含一个或多个测试。您应该将测试分为反映套件代码结构的测试套件。当测试套件中的多个测试需要共享通用对象和子例程时,可以将它们放入 test fixture 类。

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

现在,我们将说明如何编写测试程序,从各个断言级别开始,再到测试和测试套件。

# 断言

googletest 断言是类似于函数调用的宏。您可以通过断言其行为来测试类或函数。断言失败时,googletest 会输出断言的源文件和行号位置以及失败消息。您还可以提供自定义失败消息,该消息将附加到 googletest 消息中。

断言成对出现,测试相同的事物,但对当前函数有不同的影响。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;
}
1
2
3
4
5

可以流式传输到 ostream 的所有内容都可以流式传输到一个断言宏-尤其是 C 字符串和 string 对象。如果将宽字符串(wchar_t*,Windows 上的 UNICODE 模式下的 TCHAR*,或 std::wstring)流式传输到断言,则在打印时将其转换为 UTF-8。

# 基础断言

下方断言进行基本的真/假条件测试。

Fatal assertion Nonfatal assertion Verifies
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition is true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition is false

请记住,当它们失败时,ASSERT_*会导致致命故障并从当前函数返回,而 EXPECT_* 会导致非致命故障,从而允许该函数继续运行。无论哪种情况,断言失败都意味着其包含测试失败。

可用性:Linux,Windows,Mac。

# 二进制比较

本节描述比较两个值的断言。

Fatal assertion Nonfatal assertion Verifies
ASSERT_EQ(val1, val2); EXPECT_EQ(val1, val2); val1 == val2
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

值参数必须可由断言的比较运算符进行比较,否则会出现编译器错误。我们曾经要求参数支持 << 操作符以流式传输到 ostream ,但这不再是必需的。如果支持 <<,则断言失败时将调用它来打印参数;否则 googletest 会尝试以最佳方式打印它们。有关更多详细信息以及如何自定义参数的打印,请参见 文档

这些断言可以使用用户定义的类型,但前提是您定义了相应的比较运算符(例如 ==<)。由于 《Google C++样式指南》 (opens new window) 不建议这样做,因此您可能需要使用 ASSERT_TRUE()EXPECT_TRUE() 来声明用户定义类型的两个对象的相等性。

但是,在可能的情况下,首选 ASSERT_EQ(actual, expected)ASSERT_TRUE(actual == expected) 更好,因为它会告诉您 actualexpected 的失败值。

参数总是精确地评估一次。因此,参数具有副作用是可以的。但是,与任何普通的 C/C++ 函数一样,参数的评估顺序是不确定的(即,编译器可以自由选择任何顺序),并且您的代码不应依赖于任何特定的参数评估顺序。

ASSERT_EQ() 对指针进行指针相等。如果在两个 C 字符串上使用,它将测试它们是否在相同的内存位置,而不是它们的值是否相同。因此,如果要按值比较 C 字符串(例如 const char*),请使用 ASSERT_STREQ(),稍后将对此进行叙述。特别是,要判定 C 字符串为 NULL,请使用 ASSERT_STREQ(c_string,NULL)。如果支持 c++11,请考虑使用 ASSERT_EQ(c_string,nullptr)。要比较两个 string 对象,应使用 ASSERT_EQ

在进行指针比较时,请使用 *_EQ(ptr,nullptr)*_NE(ptr,nullptr) 而不是 * _EQ(ptr,NULL)* _NE(ptr,NULL)。这是因为键入了 nullptr,而没有键入 NULL。有关更多详细信息,请参见 常见问题 解答。

如果使用浮点数,则可能需要使用其中一些宏的浮点变体,以避免舍入引起的问题。有关详细信息,请参见 高级 googletest 主题

本节中的宏适用于窄和宽字符串对象 ( stringwstring )。

可用性:Linux、Windows、Mac。

历史记录:2016 年 2 月之前,*_ EQ 约定将其称为 ASSERT_EQ(expected, actual),因此许多现有代码都使用此顺序。现在,*_ EQ 以相同的方式对待两个参数。

# 字符串比较

该组中的断言 比较两个 C 字符串。如果要比较两个 string 对象,请改用 EXPECT_EQEXPECT_NE 等。

Fatal assertion Nonfatal assertion Verifies
ASSERT_STREQ(str1,str2); EXPECT_STREQ(str1,str2); the two C strings have the same content
ASSERT_STRNE(str1,str2); EXPECT_STRNE(str1,str2); the two C strings have different contents
ASSERT_STRCASEEQ(str1,str2); EXPECT_STRCASEEQ(str1,str2); the two C strings have the same content, ignoring case
ASSERT_STRCASENE(str1,str2); EXPECT_STRCASENE(str1,str2); the two C strings have different contents, ignoring case

注意,断言名称中的“CASE”表示忽略大小写。NULL 指针和空字符串被认为是不同的。

*STREQ**STRNE* 也接受宽 C 字符串(wchar_t*)。如果两个宽字符串的比较失败,则它们的值将打印为 UTF-8 窄字符串。

可用性:Linux、Windows、Mac。

另请参阅:有关更多字符串比较技巧(例如,子字符串,前缀,后缀和正则表达式匹配),请参阅《高级 googletest 指南》中的 相关内容

# 简单测试

创建测试:

  1. 使用 TEST() 宏定义和命名测试函数。这些是无返回值的普通 C++ 函数。
  2. 在此函数中,应与要包含的所有有效 C++ 语句一起使用各种 googletest 断言来检查值。
  3. 测试结果由断言确定;如果测试中的任何断言失败(致命或非致命),或者测试崩溃,则整个测试都会失败。否则,它将成功。
TEST(TestSuiteName, TestName) {
  ... test body ...
}
1
2
3

TEST() 参数从通用到特定。第一个参数是测试套件的名称,第二个参数是测试套件内的测试名称。这两个名称都必须是有效的 C++ 标识符,并且它们不应包含任何下划线(_)。测试的全名包括其包含的测试套件及其独立名称。来自不同测试套件的测试可以具有相同的独立名称。

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

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

此功能的测试套件可能类似于:

// 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);
}
1
2
3
4
5
6
7
8
9
10
11
12

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

命名测试套件和测试时,应遵循与 命名函数和类 (opens new window)(Google style) 相同的约定。

可用性:Linux、Windows、Mac。

# 多个测试使用相同的数据配置

如果发现自己编写了两个或多个对相似数据进行测试的测试,则可以使用 test fixture。这使您可以将对象的相同配置重新用于几个不同的测试。

要创建 fixture ,请执行以下操作:

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

使用 fixture 时,请使用 TEST_F() 而不是 TEST(),因为它允许您访问 test fixture 中的对象和子例程:

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

TEST() 一样,第一个参数是测试套件名称,但是对于 TEST_F(),它必须是 test fixture 类的名称。您可能已经猜到了:_F 用于 fixture。

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

另外,您必须先定义一个 test fixture 类,然后才能在 TEST_F() 中使用它,否则会出现编译器错误 "virtual outside class declaration"(“虚拟外部类声明”)。

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

例如,让我们为名为 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;
  ...
};
1
2
3
4
5
6
7
8
9

首先,定义一个 fixture 类。按照约定,您应该给它命名为 FooTest,其中 Foo 是要测试的类。

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

  // void TearDown() override {}

  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在这种情况下,不需要 TearDown() ,因为除了析构函数已经完成的工作之外,我们无需在每次测试后进行清理。

现在,我们将使用 TEST_F() 和此 fixture 编写测试。

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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

上面使用了 ASSERT_*EXPECT_* 断言。经验法则是,当您希望测试在断言失败后继续显示更多错误时,请使用 EXPECT_*;而在失败之后继续进行则没有意义时,请使用 ASSERT_*。例如,出队测试中的第二个断言是 ASSERT_NE(nullptr, n),因为我们以后需要取消对指针 n 的引用,这会在 nNULL 时导致段错误。

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

  1. googletest 构造一个 QueueTest 对象(我们称其为 t1)。
  2. t1.SetUp() 初始化 t1
  3. 第一个测试(IsEmptyInitially)在 t1 上运行。
  4. 测试完成后,t1.TearDown()执行清理。
  5. t1 被析构。
  6. 在另一个 QueueTest 对象上重复上述步骤,这次运行 DequeueWorks 测试。

可用性:Linux、Windows、Mac。

# 调用测试

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

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

调用时,RUN_ALL_TESTS() 宏:

  • 保存所有 googletest 标志的状态。
  • 为第一个测试创建 test fixture 对象。
  • 通过 SetUp() 对其进行初始化。
  • 在 fixture 对象上运行测试。
  • 通过 TearDown() 清理 fixture。
  • 删除 fixture。
  • 恢复所有 googletest 标志的状态。
  • 对下一个测试重复上述步骤,直到所有测试都已运行。

如果发生致命故障,将跳过后续步骤。

重要说明:您一定不能忽略 RUN_ALL_TESTS() 的返回值,否则会出现编译器错误。这种设计的基本原理是,自动化测试服务将根据其退出代码(而不是根据其 stdout/stderr 输出)来确定测试是否通过。因此您的 main() 函数必须返回 RUN_ALL_TESTS() 的值。

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

可用性:Linux、Windows、Mac。

# 编写 main() 函数

大多数用户不需要编写自己的 main 函数,而是与 gtest_main 链接(与 gtest 相反),其定义了合适的入口点。有关详细信息,请参见本节末尾。本节的其余部分仅适用于需要在测试运行前做一些自定义的事情而无法在 fixture 和测试套件的框架内表达的情况下。

如果编写自己的 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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

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

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

但是也许您认为编写所有这些 main 函数的工作量太大? 我们完全同意您的意见,因此 Google Test 提供了 main() 的基本实现。如果适合您的需求,则只需将测试与 gtest_main 库链接,就可以了。

注意:不推荐使用 ParseGUnitFlags(),而推荐使用 InitGoogleTest()

# 已知局限性

Google Test 被设计为线程安全的。在具有 pthreads 库的系统上,该实现是线程安全的。目前在其他系统(例如 Windows )上同时使用两个线程的 Google Test 断言是不安全的。在大多数测试中,这不是问题,因为断言通常是在主线程中完成的。如果需要帮助,您可以自愿在平台的 gtest-port.h 中实现必要的同步原语。

Last Updated: 2023-10-29T08:26:04.000Z