Google Test 高级主题

1 引言

既然你已经阅读了 GoogleTest 入门指南,并学会了如何使用 GoogleTest 编写测试,那么现在是时候学习一些新技巧了。本文将向你展示更多断言的用法,以及如何构建复杂的失败消息、传播致命失败、重用和加速测试夹具,以及如何使用各种标志与测试配合使用。

2 更多断言

本节介绍了一些不那么常用但仍然重要的断言。

2.1 显式成功和失败

请参阅 断言参考 中的“显式成功和失败”部分。

2.2 异常断言

请参阅 断言参考 中的“异常断言”部分。

2.3 使用谓词断言以获得更好的错误消息

尽管 GoogleTest 提供了丰富的断言,但它们永远无法涵盖所有场景,因为不可能(也不建议)预见用户可能遇到的所有情况。因此,有时用户不得不使用 EXPECT_TRUE() 来检查复杂的表达式,因为没有更好的宏可用。这有一个问题,就是它不会显示表达式各部分的值,使得很难理解出了什么问题。作为权宜之计,一些用户选择自己构造失败消息,将其流式传输到 EXPECT_TRUE() 中。然而,当表达式具有副作用或计算成本较高时,这会显得非常笨拙。

GoogleTest 提供了三种不同的方法来解决这个问题:

2.3.1 使用现有的布尔函数

如果你已经有一个返回 bool(或可以隐式转换为 bool 的类型)的函数或函数对象,你可以在 谓词断言 中使用它,以免费获取函数参数的打印信息。请参阅 断言参考 中的 EXPECT_PRED*

2.3.2 使用返回 AssertionResult 的函数

尽管 EXPECT_PRED*() 和类似宏对于快速任务很方便,但语法并不令人满意:你需要为不同数量的参数使用不同的宏,而且它更像是 Lisp 而不是 C++。::testing::AssertionResult 类解决了这个问题。

AssertionResult 对象表示断言的结果(它是成功还是失败,以及相关的消息)。你可以使用以下工厂函数之一来创建 AssertionResult

namespace testing {

// 返回一个 AssertionResult 对象,表示断言已成功。
AssertionResult AssertionSuccess();

// 返回一个 AssertionResult 对象,表示断言已失败。
AssertionResult AssertionFailure();

}

然后,你可以使用 << 运算符将消息流式传输到 AssertionResult 对象中。

为了在布尔断言(例如 EXPECT_TRUE())中提供更易读的消息,编写一个返回 AssertionResult 而不是 bool 的谓词函数。例如,如果你定义 IsEven() 如下:

testing::AssertionResult IsEven(int n) {
  if ((n % 2) == 0)
    return testing::AssertionSuccess();
  else
    return testing::AssertionFailure() << n << " 是奇数";
}

而不是:

bool IsEven(int n) {
  return (n % 2) == 0;
}

那么失败的断言 EXPECT_TRUE(IsEven(Fib(4))) 将打印:

Value of: IsEven(Fib(4))
  Actual: false (3 是奇数)
Expected: true

而不是更晦涩的:

Value of: IsEven(Fib(4))
  Actual: false
Expected: true

如果你希望在 EXPECT_FALSEASSERT_FALSE 中也有信息性的消息(Google 代码库中大约三分之一的布尔断言是否定的),并且你愿意在成功的情况下使谓词变慢,那么你可以提供一个成功消息:

testing::AssertionResult IsEven(int n) {
  if ((n % 2) == 0)
    return testing::AssertionSuccess() << n << " 是偶数";
  else
    return testing::AssertionFailure() << n << " 是奇数";
}

然后,语句 EXPECT_FALSE(IsEven(Fib(6))) 将打印:

  Value of: IsEven(Fib(6))
     Actual: true (8 是偶数)
  Expected: false

2.3.3 使用谓词格式化器

如果你对 EXPECT_PRED*EXPECT_TRUE 生成的默认消息不满意,或者你的谓词的一些参数不支持流式传输到 ostream,你可以改用 谓词格式化器断言 来 完全 自定义消息的格式。请参阅 断言参考 中的 EXPECT_PRED_FORMAT*

2.4 浮点数比较

请参阅 断言参考 中的“浮点数比较”部分。

2.4.1 浮点数谓词格式函数

一些浮点数操作很有用,但并不常用。为了避免引入大量新宏,我们提供了一些可以作为谓词断言宏 EXPECT_PRED_FORMAT2 的参数使用的谓词格式函数,例如:

using ::testing::FloatLE;
using ::testing::DoubleLE;
...
EXPECT_PRED_FORMAT2(FloatLE, val1, val2);
EXPECT_PRED_FORMAT2(DoubleLE, val1, val2);

上述代码验证 val1 是否小于或近似等于 val2

2.5 使用 gMock 匹配器进行断言

请参阅 断言参考 中的 EXPECT_THAT

2.6 更多字符串断言

(如果你还没有阅读,请先阅读上一节。)

你可以使用 gMock 字符串匹配器与 EXPECT_THAT 配合,以实现更多字符串比较技巧(子字符串、前缀、后缀、正则表达式等)。例如:

using ::testing::HasSubstr;
using ::testing::MatchesRegex;
...
ASSERT_THAT(foo_string, HasSubstr("needle"));
EXPECT_THAT(bar_string, MatchesRegex("\\w*\\d+"));

2.7 Windows HRESULT 断言

请参阅 断言参考 中的“Windows HRESULT 断言”部分。

2.8 类型断言

你可以调用函数

::testing::StaticAssertTypeEq<T1, T2>();

来断言类型 T1T2 是否相同。如果断言满足,该函数什么也不做。如果类型不同,函数调用将无法编译,编译器错误消息将指出“T1T2 不是同一种类型”,并且大多数情况下(取决于编译器),它会显示 T1T2 的实际值。这在模板代码中特别有用。

注意:当在类模板或函数模板的成员函数中使用时,StaticAssertTypeEq<T1, T2>() 只有在该函数被实例化时才有效。例如,给定:

template <typename T> class Foo {
 public:
  void Bar() { testing::StaticAssertTypeEq<int, T>(); }
};

代码:

void Test1() { Foo<bool> foo; }

不会产生编译器错误,因为 Foo<bool>::Bar() 实际上从未被实例化。相反,你需要:

void Test2() { Foo<bool> foo; foo.Bar(); }

才会导致编译器错误。

警告:在构造函数或析构函数中使用致命断言(例如 FAIL*ASSERT_*)不会终止当前测试,正如你可能认为的那样:它只是提前从构造函数或析构函数返回,可能会使你的对象处于部分构造或部分析构的状态!你几乎肯定想要调用 abort 或使用 SetUp/TearDown

3 断言放置

你可以将断言用于任何 C++ 函数。它不一定非要是测试夹具类的方法。唯一的限制是,生成致命失败(FAIL*ASSERT_*)的断言只能用于返回 void 的函数。这是由于 Google 不使用异常。如果你在非 void 函数中使用它,你会得到一个令人困惑的编译错误,例如“error: void value not ignored as it ought to be”或“cannot initialize return object of type 'bool' with an rvalue of type 'void'”或“error: no viable conversion from 'void' to 'string'”。

如果你需要在返回非 void 的函数中使用致命断言,一个选项是将函数改为通过输出参数返回值。例如,你可以将 T2 Foo(T1 x) 重写为 void Foo(T1 x, T2* result)。你需要确保即使函数提前返回,*result 也包含一些合理的值。由于函数现在返回 void,你可以在其中使用任何断言。

如果无法更改函数的类型,你应该只使用生成非致命失败的断言,例如 ADD_FAILURE*EXPECT_*

注意:根据 C++ 语言规范,构造函数和析构函数不被视为返回 void 的函数,因此你不能在它们中使用致命断言;如果你尝试这样做,你会得到一个编译错误。相反,你可以调用 abort 并使整个测试程序崩溃,或者将致命断言放入 SetUp/TearDown 函数中;请参阅 构造函数/析构函数与 SetUp/TearDown

4 跳过测试执行

与断言 SUCCEED()FAIL() 类似,你可以使用 GTEST_SKIP() 宏在运行时阻止进一步的测试执行。当你需要在运行时检查被测试系统的前置条件并以有意义的方式跳过测试时,这非常有用。

GTEST_SKIP() 可以在单独的测试用例中使用,也可以在派生自 ::testing::Environment::testing::Test 的类的 SetUp() 方法中使用。例如:

TEST(SkipTest, DoesSkip) {
  GTEST_SKIP() << "跳过单个测试";
  FAIL();  // 不会失败;它不会被执行
}

class SkipFixture : public ::testing::Test {
 protected:
  void SetUp() override {
    GTEST_SKIP() << "跳过此夹具的所有测试";
  }
};

// SkipFixture 的测试不会被执行。
TEST_F(SkipFixture, SkipsOneTest) {
  FAIL();  // 不会失败;它不会被执行
}

与断言宏类似,你也可以将自定义消息流式传输到 GTEST_SKIP() 中。

5 教授 GoogleTest 如何打印你的值

当测试断言(如 EXPECT_EQ)失败时,GoogleTest 会打印参数值以帮助你调试。它通过一个可扩展的值打印机来实现这一点。

该打印机知道如何打印内置的 C++ 类型、原生数组、STL 容器,以及任何支持 << 运算符的类型。对于其他类型,它会打印值的原始字节,并希望你能从中推断出结果。

正如前面提到的,打印机是 可扩展的。也就是说,你可以教它以比转储字节更好的方式打印你的特定类型。为此,请为你的类型定义一个 AbslStringify() 重载作为 friend 函数模板:

namespace foo {

class Point {  // 我们希望 GoogleTest 能够打印此类的实例。
  ...
  // 提供一个 friend 重载。
  template <typename Sink>
  friend void AbslStringify(Sink& sink, const Point& point) {
    absl::Format(&sink, "(%d, %d)", point.x, point.y);
  }

  int x;
  int y;
};

// 如果你不能在类中声明该函数,重要的是要在定义 Point 的同一个命名空间中定义 AbslStringify() 的重载。
// C++ 的查找规则依赖于此。
enum class EnumWithStringify { kMany = 0, kChoices = 1 };

template <typename Sink>
void AbslStringify(Sink& sink, EnumWithStringify e) {
  absl::Format(&sink, "%s", e == EnumWithStringify::kMany ? "Many" : "Choices");
}

}  // namespace foo

注意:AbslStringify() 利用一个通用的“sink”缓冲区来构造字符串。关于 AbslStringify() 的 sink 支持的操作的更多信息,请参阅 AbslStringify() 文档

AbslStringify() 还可以使用 absl::StrFormat 的通用 %v 类型说明符在其自己的格式字符串中执行类型推导。上面的 Point 可以格式化为 "(%v, %v)",例如,并推导出 int 值作为 %d

有时,AbslStringify() 可能不是一个选项:你的团队可能希望仅用于测试目的的打印类型时带有额外的调试信息。如果是这样,你可以改用定义一个 PrintTo() 函数,如下所示:

#include <ostream>

namespace foo {

class Point {
  ...
  friend void PrintTo(const Point& point, std::ostream* os) {
    *os << "(" << point.x << "," << point.y << ")";
  }

  int x;
  int y;
};

// 如果你不能在类中声明该函数,重要的是要在定义 Point 的同一个命名空间中定义 PrintTo()。
// C++ 的查找规则依赖于此。
void PrintTo(const Point& point, std::ostream* os) {
    *os << "(" << point.x << "," << point.y << ")";
}

}  // namespace foo

如果你同时定义了 AbslStringify()PrintTo(),那么后者将被 GoogleTest 使用。这允许你自定义值在 GoogleTest 输出中的显示方式,而不会影响依赖于 AbslStringify() 行为的代码。

如果你已经有一个现有的 << 运算符,并且想要定义一个 AbslStringify(),那么后者将用于 GoogleTest 打印。

如果你想使用 GoogleTest 的值打印机自己打印一个值 x,只需调用 ::testing::PrintToString(x),它返回一个 std::string

std::vector<std::pair<Point, int>> point_ints = GetPointIntVector();

EXPECT_TRUE(IsCorrectPointIntVector(point_ints))
    << "point_ints = " << testing::PrintToString(point_ints);

有关 AbslStringify() 及其与其他库的集成的更多详细信息,请参阅 文档

6 正则表达式语法

当使用 Bazel 构建且使用 Abseil 时,GoogleTest 使用 RE2 语法。否则,在 POSIX 系统(Linux、Cygwin、Mac)上,GoogleTest 使用 POSIX 扩展正则表达式语法。要了解 POSIX 语法,你可能想阅读这个 Wikipedia 条目

在 Windows 上,GoogleTest 使用它自己的简单正则表达式实现。它缺乏许多功能。例如,我们不支持联合("x|y")、分组("(xy)")、字符集("[xy]")、重复计数("x{5,7}")等。以下是我们的支持内容(A 表示一个字面字符、点(.)或一个单独的 \\ 转义序列;xy 表示正则表达式):

表达式 含义 c 匹配任何字面字符 c \\d 匹配任何十进制数字 \\D 匹配任何非十进制数字的字符 \\f 匹配 \f \\n 匹配 \n \\r 匹配 \r \\s 匹配任何 ASCII 空白字符,包括 \n \\S 匹配任何非空白字符 \\t 匹配 \t \\v 匹配 \v \\w 匹配任何字母、_ 或十进制数字 \\W 匹配 \\w 不匹配的任何字符 \\c 匹配任何字面字符 c,它必须是一个标点符号 . 匹配除 \n 以外的任何单个字符 A? 匹配 A 的 0 次或 1 次出现 A* 匹配 A 的 0 次或多次出现 A+ 匹配 A 的 1 次或多次出现 ^ 匹配字符串的开头(而不是每行的开头) $ 匹配字符串的结尾(而不是每行的结尾) xy 匹配 x 后跟 y

为了帮助你确定你的系统上可用的功能,GoogleTest 定义了宏来控制它使用哪种正则表达式。这些宏是:GTEST_USES_SIMPLE_RE=1GTEST_USES_POSIX_RE=1。如果你想让你的死亡测试在所有情况下都能工作,你可以对这些宏进行 #if 判断,或者只使用更有限的语法。

7 死亡测试

在许多应用程序中,有一些断言可以在条件不满足时导致应用程序失败。这些一致性检查确保程序处于已知的良好状态,并且在程序状态被破坏后尽早失败。如果断言检查了错误的条件,那么程序可能会以错误的状态继续执行,这可能导致内存损坏、安全漏洞或更糟的情况。因此,测试这些断言语句是否按预期工作至关重要。

由于这些前置检查会导致进程死亡,我们将此类测试称为 死亡测试。更一般地说,任何测试检查程序是否以预期的方式终止(除了通过抛出异常)也是死亡测试。

请注意,如果一段代码抛出异常,我们不认为它“死亡”,因为死亡测试的目的不是如此,因为调用代码的调用者可以捕获异常并避免崩溃。如果你想验证你的代码抛出的异常,请参阅 异常断言

如果你想测试测试代码中的 EXPECT_*()ASSERT_*() 失败,请参阅“捕获”失败。

7.1 如何编写死亡测试

GoogleTest 提供了断言宏来支持死亡测试。请参阅 断言参考 中的“死亡断言”部分。

要编写死亡测试,只需在测试函数中使用其中一个宏。例如:

TEST(MyDeathTest, Foo) {
  // 这个死亡测试使用了一个复合语句。
  ASSERT_DEATH({
    int n = 5;
    Foo(&n);
  }, "Error on line .* of Foo()");
}

TEST(MyDeathTest, NormalExit) {
  EXPECT_EXIT(NormalExit(), testing::ExitedWithCode(0), "Success");
}

TEST(MyDeathTest, KillProcess) {
  EXPECT_EXIT(KillProcess(), testing::KilledBySignal(SIGKILL),
              "Sending myself unblockable signal");
}

验证:

  • 调用 Foo(5) 会导致进程以给定的错误消息死亡,
  • 调用 NormalExit() 会导致进程打印 "Success" 到 stderr 并以退出代码 0 退出,以及
  • 调用 KillProcess() 会以信号 SIGKILL 杀死进程。

警告:如果你的死亡测试包含模拟对象并且期望特定的退出代码,那么你必须通过 Mock::AllowLeak 允许模拟对象泄漏。这是因为如果模拟泄漏检测器检测到泄漏,它将退出并带有自己的错误代码。

测试函数体还可以包含其他断言和语句,如果需要的话。

请注意,死亡测试只关心三件事:

  1. statement 是否会终止或退出进程?
  2. (在 ASSERT_EXITEXPECT_EXIT 的情况下)退出状态是否满足 predicate?或者(在 ASSERT_DEATHEXPECT_DEATH 的情况下)退出状态是否非零?以及
  3. 标准错误输出是否与 matcher 匹配?

特别是,如果 statement 生成了一个 ASSERT_*EXPECT_* 失败,它 不会 导致死亡测试失败,因为 GoogleTest 断言不会终止进程。

7.2 死亡测试命名

重要:我们强烈建议你遵循将 测试套件(而不是测试)命名为 *DeathTest 的约定,如上面的例子所示。下面的“死亡测试和线程”部分解释了原因。

如果一个测试夹具类被普通测试和死亡测试共享,你可以使用 usingtypedef 为夹具类引入一个别名,以避免重复它的代码:

class FooTest : public testing::Test { ... };

using FooDeathTest = FooTest;

TEST_F(FooTest, DoesThis) {
  // 普通测试
}

TEST_F(FooDeathTest, DoesThat) {
  // 死亡测试
}

7.3 它是如何工作的

请参阅 断言参考 中的“死亡断言”部分。

7.4 死亡测试和线程

引入两种死亡测试样式的理由与线程安全性有关。由于众所周知的在多线程环境中使用 fork() 的问题,死亡测试应该在单线程上下文中运行。然而,有时,安排这种环境并不现实。例如,静态初始化的模块可能在到达 main 之前就启动线程。一旦线程被创建,可能很难甚至不可能清理它们。

GoogleTest 有三个特性旨在提高对线程问题的意识。

  1. 如果在遇到死亡测试时有多个线程正在运行,会发出警告。
  2. 以“DeathTest”结尾的测试套件会在所有其他测试之前运行。
  3. 在 Linux 上(在 Cygwin 和 Mac 上不可用),它使用 clone() 而不是 fork() 来派生子进程,因为 fork() 更可能导致子进程在父进程有多个线程时挂起。

在实现细节上,你可以在死亡测试语句中创建线程;它们在单独的进程中执行,因此不会影响父进程。

7.5 死亡测试样式

“线程安全”的死亡测试样式是为了帮助缓解在可能的多线程环境中进行测试的风险而引入的。它以增加测试执行时间(可能显著增加)为代价,换取改进的线程安全性。

自动化测试框架不会设置样式标志。你可以通过编程方式设置标志来选择特定的死亡测试样式:

GTEST_FLAG_SET(death_test_style, "threadsafe");

你可以在 main() 中这样做,以设置二进制文件中所有死亡测试的样式,或者在单独的测试中这样做。回想一下,标志在运行每个测试之前都会被保存,并在之后恢复,所以你不需要自己这样做。例如:

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

TEST(MyDeathTest, TestOne) {
  GTEST_FLAG_SET(death_test_style, "threadsafe");
  // 这个测试以“线程安全”样式运行:
  ASSERT_DEATH(ThisShouldDie(), "");
}

TEST(MyDeathTest, TestTwo) {
  // 这个测试以“快速”样式运行:
  ASSERT_DEATH(ThisShouldDie(), "");
}

7.6 注意事项

statement 参数可以是任何有效的 C++ 语句。如果它通过 return 语句或抛出异常离开当前函数,死亡测试将被视为失败。一些 GoogleTest 宏可能会从当前函数返回(例如 ASSERT_TRUE()),因此请确保在 statement 中不要使用它们。

由于 statement 在子进程中运行,它引起的任何内存副作用(例如修改变量、释放内存等)将 不会 在父进程中被观察到。特别是,如果你在死亡测试中释放内存,你的程序将因堆检查失败而失败,因为父进程永远不会看到内存被回收。为了解决这个问题,你可以:

  1. 尝试不在死亡测试中释放内存;
  2. 再次在父进程中释放内存;或者
  3. 不要在你的程序中使用堆检查器。

由于实现细节,你不能在同一线上放置多个死亡测试断言;否则,编译将失败,并出现不明显的错误消息。

尽管“线程安全”样式的死亡测试提供了改进的线程安全性,但在存在使用 pthread_atfork(3) 注册的处理器的情况下,仍然可能出现线程问题,例如死锁。

8 在子程序中使用断言

注意:如果你想将一系列测试断言放入一个子程序中以检查复杂的条件,考虑使用 自定义 GMock 匹配器。这可以让你在失败时提供更易读的错误消息,并避免下面描述的所有问题。

8.1 为断言添加跟踪信息

如果一个测试子程序从多个地方被调用,当子程序中的断言失败时,很难判断失败来自哪个调用。你可以通过额外的日志记录或自定义失败消息来缓解这个问题,但这通常会使测试变得杂乱。更好的解决方案是使用 SCOPED_TRACE 宏或 ScopedTrace 实用工具:

ScopedTrace trace("file_path", line_number, message);

其中 message 可以是任何可以流式传输到 std::ostream 的内容。SCOPED_TRACE 宏将在每个失败消息中添加当前文件名、行号以及给定的消息。ScopedTrace 接受显式的文件名和行号作为参数,这对于编写测试辅助函数很有用。当控制离开当前词法作用域时,效果将被撤销。

例如:

10: void Sub1(int n) {
11:   EXPECT_EQ(Bar(n), 1);
12:   EXPECT_EQ(Bar(n + 1), 2);
13: }
14:
15: TEST(FooTest, Bar) {
16:   {
17:     SCOPED_TRACE("A");  // 这个跟踪点将被包含在
18:                         // 此作用域中的每个失败消息中。
19:     Sub1(1);
20:   }
21:   // 现在不会了。
22:   Sub1(9);
23: }

可能会导致这样的消息:

path/to/foo_test.cc:11: Failure
Value of: Bar(n)
Expected: 1
  Actual: 2
Google Test trace:
path/to/foo_test.cc:17: A

path/to/foo_test.cc:12: Failure
Value of: Bar(n + 1)
Expected: 2
  Actual: 3

如果没有跟踪,很难知道两个失败分别来自哪个对 Sub1() 的调用。(你可以在 Sub1() 中的每个断言中添加一个额外的消息来指示 n 的值,但这很繁琐。)

使用 SCOPED_TRACE 的一些提示:

  1. 使用合适的消息时,通常只需在子程序的开头使用 SCOPED_TRACE,而不是在每个调用点。
  2. 当在循环中调用子程序时,将循环迭代器包含在 SCOPED_TRACE 的消息中,这样你就可以知道失败来自哪次迭代。
  3. 有时跟踪点的行号就足以识别特定的子程序调用。在这种情况下,你不需要为 SCOPED_TRACE 选择一个唯一的消息。你只需使用 ""
  4. 你可以在内部作用域中使用 SCOPED_TRACE,即使外部作用域中已经有一个。在这种情况下,所有活动的跟踪点都将被包含在失败消息中,按它们被遇到的相反顺序排列。
  5. 在 Emacs 中,跟踪转储是可点击的——在行号上按回车键,你将被带到源文件中的那行!

8.2 传播致命失败

使用 ASSERT_*FAIL* 的一个常见陷阱是不了解它们失败时只终止 当前函数,而不是整个测试。例如,以下测试会导致段错误:

void Subroutine() {
  // 生成一个致命失败并终止当前函数。
  ASSERT_EQ(1, 2);

  // 下面的代码不会被执行。
  ...
}

TEST(FooTest, Bar) {
  Subroutine();  // 打算的行为是子程序中的致命失败
                 // 终止整个测试。

  // 实际行为:函数在 Subroutine() 返回后继续执行。
  int* p = nullptr;
  *p = 3;  // 段错误!
}

为了解决这个问题,GoogleTest 提供了三种不同的解决方案。你可以使用异常、(ASSERT|EXPECT)_NO_FATAL_FAILURE 断言或 HasFatalFailure() 函数。这些内容在以下两个小节中介绍。

8.2.1 在子程序中使用异常断言

以下代码可以将 ASSERT 失败转换为异常:

class ThrowListener : public testing::EmptyTestEventListener {
  void OnTestPartResult(const testing::TestPartResult& result) override {
    if (result.type() == testing::TestPartResult::kFatalFailure) {
      throw testing::AssertionException(result);
    }
  }
};
int main(int argc, char** argv) {
  ...
  testing::UnitTest::GetInstance()->listeners().Append(new ThrowListener);
  return RUN_ALL_TESTS();
}

如果添加了其他监听器,应该在它们之后添加此监听器,否则它们将看不到失败的 OnTestPartResult

8.2.2 在子程序中使用断言

如上所示,如果测试调用了一个包含 ASSERT_* 失败的子程序,测试将继续在子程序返回后执行。这可能不是你想要的。

通常,人们希望致命失败像异常一样传播。为此,GoogleTest 提供了以下宏:

致命断言 非致命断言 验证 ASSERT_NO_FATAL_FAILURE(statement); EXPECT_NO_FATAL_FAILURE(statement); statement 在当前线程中没有生成任何新的致命失败。

只有在执行断言的线程中发生的失败才会被检查以确定这种类型断言的结果。如果 statement 创建了新线程,这些线程中的失败将被忽略。

示例:

ASSERT_NO_FATAL_FAILURE(Foo());

int i;
EXPECT_NO_FATAL_FAILURE({
  i = Bar();
});

在 Windows 上,目前不支持从多个线程中进行断言。

8.2.3 检查当前测试中的失败

::testing::Test 类中的 HasFatalFailure() 方法返回 true,如果当前测试中的某个断言遭受了致命失败。这允许函数捕获子程序中的致命失败并提前返回。

class Test {
 public:
  ...
  static bool HasFatalFailure();
};

典型的用法(基本上模拟了抛出异常的行为)是:

TEST(FooTest, Bar) {
  Subroutine();
  // 如果 Subroutine() 有一个致命失败,则终止。
  if (HasFatalFailure()) return;

  // 下面的代码不会被执行。
  ...
}

如果在 TEST()TEST_F() 或测试夹具类之外使用 HasFatalFailure(),你必须添加 ::testing::Test:: 前缀,如下所示:

if (testing::Test::HasFatalFailure()) return;

类似地,HasNonfatalFailure() 返回 true 如果当前测试至少有一个非致命失败,HasFailure() 返回 true 如果当前测试至少有一个任何类型的失败。

9 记录额外信息

在测试代码中,你可以调用 RecordProperty("key", value) 来记录额外的信息,其中 value 可以是字符串或 int。如果指定了 XML 输出,最后为某个键记录的值将被输出到 XML 中。例如,测试

TEST_F(WidgetUsageTest, MinAndMaxWidgets) {
  RecordProperty("MaximumWidgets", ComputeMaxUsage());
  RecordProperty("MinimumWidgets", ComputeMinUsage());
}

将输出类似以下的 XML:

  ...
    <testcase name="MinAndMaxWidgets" file="test.cpp" line="1" status="run" time="0.006" classname="WidgetUsageTest" MaximumWidgets="12" MinimumWidgets="9" />
  ...

注意:

  • RecordProperty()Test 类的静态成员。因此,如果在测试体和测试夹具类之外使用,需要加上 ::testing::Test:: 前缀。
  • key 必须是有效的 XML 属性名称,并且不能与 GoogleTest 已经使用的属性冲突(namestatustimeclassnametype_paramvalue_param)。
  • 在测试生命周期之外调用 RecordProperty() 是允许的。如果它在测试套件的 SetUpTestSuite()TearDownTestSuite() 方法之间被调用,它将被归属于测试套件的 XML 元素。如果它在所有测试套件之外(例如在测试环境中)被调用,它将被归属于顶层 XML 元素。

10 在同一个测试套件中的测试之间共享资源

GoogleTest 为每个测试创建一个新的测试夹具对象,以使测试独立且易于调试。然而,有时测试使用的资源设置成本较高,使得每个测试一个副本的模型过于昂贵。

如果测试不改变资源的状态,那么它们共享一个资源副本是没有问题的。因此,除了每个测试的设置/拆除之外,GoogleTest 还支持每个测试套件的设置/拆除。为此:

  1. 在你的测试夹具类(假设为 FooTest)中,声明一些静态成员变量来保存共享资源。
  2. 在测试夹具类之外(通常紧随其后),定义这些成员变量,可以选择性地为它们赋予初始值。
  3. 在同一测试夹具类中,定义一个公共成员函数 static void SetUpTestSuite()(注意不要拼写成 SetupTestSuite,小写 u!)来设置共享资源,以及一个 static void TearDownTestSuite() 函数来拆除它们。

就这样!GoogleTest 会在运行 FooTest 测试套件中的 第一个测试(即在创建第一个 FooTest 对象之前)时自动调用 SetUpTestSuite(),并在运行 最后一个测试(即在删除最后一个 FooTest 对象之后)时调用 TearDownTestSuite()。在这两者之间,测试可以使用共享资源。

请记住,测试的顺序是未定义的,因此你的代码不能依赖于一个测试在另一个测试之前或之后执行。此外,测试要么不修改任何共享资源的状态,要么,如果它们确实修改了状态,它们必须在将控制权传递给下一个测试之前将状态恢复到原始值。

请注意,SetUpTestSuite() 可能会被调用多次,对于有派生类的测试夹具类来说尤其如此,因此你不应该期望函数体中的代码只运行一次。此外,派生类仍然可以访问作为静态成员定义的共享资源,因此在管理共享资源时需要谨慎,以避免在 TearDownTestSuite() 中没有正确清理共享资源而导致的内存泄漏。

下面是一个测试套件级设置和拆除的示例:

class FooTest : public testing::Test {
 protected:
  // 测试套件级设置。
  // 在此测试套件的第一个测试运行之前调用。
  // 如果不需要,可以省略。
  static void SetUpTestSuite() {
    shared_resource_ = new ...;

    // 如果 `shared_resource_` **在 `TearDownTestSuite()` 中没有被删除**,
    // 应该防止重新分配,因为 `SetUpTestSuite()` 可能会在 `FooTest` 的派生类中被调用,
    // 这可能导致内存泄漏。
    //
    // if (shared_resource_ == nullptr) {
    //   shared_resource_ = new ...;
    // }
  }

  // 测试套件级拆除。
  // 在此测试套件的最后一个测试运行之后调用。
  // 如果不需要,可以省略。
  static void TearDownTestSuite() {
    delete shared_resource_;
    shared_resource_ = nullptr;
  }

  // 你可以像往常一样定义每个测试的设置逻辑。
  void SetUp() override { ... }

  // 你可以像往常一样定义每个测试的拆除逻辑。
  void TearDown() override { ... }

  // 所有测试共享的昂贵资源。
  static T* shared_resource_;
};

T* FooTest::shared_resource_ = nullptr;

TEST_F(FooTest, Test1) {
  ... 你可以在这里引用 shared_resource_ ...
}

TEST_F(FooTest, Test2) {
  ... 你可以在这里引用 shared_resource_ ...
}

注意:尽管上面的代码将 SetUpTestSuite() 声明为 protected,但有时可能需要将其声明为 public,例如当与 TEST_P 一起使用时。

11 全局设置和拆除

正如你可以在测试级别和测试套件级别进行设置和拆除一样,你也可以在测试程序级别进行。方法如下。

首先,你通过从 ::testing::Environment 类派生来定义一个测试环境,它知道如何进行设置和拆除:

class Environment : public ::testing::Environment {
 public:
  ~Environment() override {}

  // 覆盖此方法以定义如何进行设置。
  void SetUp() override {}

  // 覆盖此方法以定义如何进行拆除。
  void TearDown() override {}
};

然后,通过调用 ::testing::AddGlobalTestEnvironment() 函数,将你的环境类的一个实例注册到 GoogleTest 中:

Environment* AddGlobalTestEnvironment(Environment* env);

现在,当调用 RUN_ALL_TESTS() 时,它首先会调用 SetUp() 方法。然后执行测试,前提是没有任何环境报告致命失败且没有调用 GTEST_SKIP()。最后,调用 TearDown()

请注意,只有至少有一个测试要执行时,才会调用 SetUp()TearDown()。重要的是,即使由于致命失败或 GTEST_SKIP() 而没有运行测试,也会调用 TearDown()

是否在每次迭代中调用 SetUp()TearDown() 取决于标志 gtest_recreate_environments_when_repeating。如果为每个迭代重新创建测试环境,则在每次迭代中为每个环境对象调用 SetUp()TearDown()。然而,如果在每次迭代中不重新创建测试环境,则仅在第一次迭代中调用 SetUp(),仅在最后一次迭代中调用 TearDown()

你可以注册多个环境对象。在这个套件中,它们的 SetUp() 将按注册顺序被调用,它们的 TearDown() 将按相反顺序被调用。

请注意,GoogleTest 拥有注册的环境对象的所有权。因此 不要自己删除它们。

你应该在调用 RUN_ALL_TESTS() 之前调用 AddGlobalTestEnvironment(),可能在 main() 中。如果你使用 gtest_main,你需要在 main() 开始之前调用它,以便它生效。实现这一点的一种方法是定义一个全局变量,如下所示:

testing::Environment* const foo_env =
    testing::AddGlobalTestEnvironment(new FooEnvironment);

然而,我们强烈建议你编写自己的 main() 并在那里调用 AddGlobalTestEnvironment(),因为依赖于全局变量的初始化会使代码更难阅读,并且可能会在从不同翻译单元注册多个环境且这些环境之间存在依赖关系时出现问题(记住,编译器不会保证来自不同翻译单元的全局变量的初始化顺序)。

12 值参数化测试

值参数化测试 允许你用不同的参数测试代码,而无需为每种参数编写多个相同的测试。这在多种情况下非常有用,例如:

  • 你有一些代码,其行为受一个或多个命令行标志的影响。你希望确保你的代码对这些标志的各种值都能正确执行。
  • 你想要测试不同实现的面向对象接口。
  • 你想要对各种输入进行代码测试(也称为数据驱动测试)。这种特性很容易被滥用,所以请在使用时发挥你的判断力!

12.1 如何编写值参数化测试

要编写值参数化测试,首先你需要定义一个夹具类。它必须同时从 testing::Testtesting::WithParamInterface<T>(后者是一个纯接口)派生,其中 T 是你的参数值类型。为了方便起见,你可以直接从 testing::TestWithParam<T> 派生夹具类,它本身同时从 testing::Testtesting::WithParamInterface<T> 派生。T 可以是任何可复制的类型。如果它是原始指针,你需要负责管理所指向值的生命周期。

注意:如果你的测试夹具定义了 SetUpTestSuite()TearDownTestSuite(),它们必须被声明为 public 而不是 protected,以便使用 TEST_P

class FooTest :
    public testing::TestWithParam<absl::string_view> {
  // 你可以像往常一样在夹具类中实现所有通常的成员。
  // 要访问测试参数,请在 `TestWithParam<T>` 类中调用 `GetParam()` 方法:
};

// 或者,当你想为一个已经存在的夹具类添加参数时:
class BaseTest : public testing::Test {
  ...
};
class BarTest : public BaseTest,
                public testing::WithParamInterface<absl::string_view> {
  ...
};

然后,使用 TEST_P 宏定义尽可能多的测试模式,使用这个夹具。_P 后缀代表“参数化”或“模式”,随你喜好选择。

TEST_P(FooTest, DoesBlah) {
  // 在测试中,通过 `TestWithParam<T>` 类的 `GetParam()` 方法访问测试参数:
  EXPECT_TRUE(foo.Blah(GetParam()));
  ...
}

TEST_P(FooTest, HasBlahBlah) {
  ...
}

最后,你可以使用 INSTANTIATE_TEST_SUITE_P 宏,使用你想要的任何参数集来实例化测试套件。GoogleTest 定义了一系列函数,用于生成测试参数——请参阅 INSTANTIATE_TEST_SUITE_P测试参考 中的详细信息。

例如,以下语句将使用 Values 参数生成器,为每个参数值 "meeny""miny""moe" 实例化来自 FooTest 测试套件的测试:

INSTANTIATE_TEST_SUITE_P(MeenyMinyMoe,
                         FooTest,
                         testing::Values("meeny", "miny", "moe"));

注意:上面的代码必须放在全局或命名空间作用域中,而不是函数作用域中。

INSTANTIATE_TEST_SUITE_P 的第一个参数是实例化测试套件的唯一名称。接下来的参数是测试模式的名称,最后一个参数是参数生成器。

参数生成器表达式不会在 GoogleTest 初始化(通过 InitGoogleTest())之前被评估。在 main 函数中进行的任何初始化(例如,标志解析的结果)都可以在参数生成器中使用。

你可以多次实例化一个测试模式,因此为了区分不同实例的模式,实例化名称被添加为实际测试套件名称的前缀。记得为不同的实例选择唯一的前缀。上面的实例化中的测试将有以下名称:

  • MeenyMinyMoe/FooTest.DoesBlah/0 对于 "meeny"
  • MeenyMinyMoe/FooTest.DoesBlah/1 对于 "miny"
  • MeenyMinyMoe/FooTest.DoesBlah/2 对于 "moe"
  • MeenyMinyMoe/FooTest.HasBlahBlah/0 对于 "meeny"
  • MeenyMinyMoe/FooTest.HasBlahBlah/1 对于 "miny"
  • MeenyMinyMoe/FooTest.HasBlahBlah/2 对于 "moe"

你可以在 --gtest_filter 中使用这些名称。

以下语句将再次实例化所有来自 FooTest 的测试,每个参数值 "cat""dog" 使用 ValuesIn 参数生成器:

constexpr absl::string_view kPets[] = {"cat", "dog"};
INSTANTIATE_TEST_SUITE_P(Pets, FooTest, testing::ValuesIn(kPets));

来自上述实例化的测试将具有以下名称:

  • Pets/FooTest.DoesBlah/0 对于 "cat"
  • Pets/FooTest.DoesBlah/1 对于 "dog"
  • Pets/FooTest.HasBlahBlah/0 对于 "cat"
  • Pets/FooTest.HasBlahBlah/1 对于 "dog"

请注意,INSTANTIATE_TEST_SUITE_P 将实例化给定测试套件中的 所有 测试,无论它们的定义是在 INSTANTIATE_TEST_SUITE_P 语句之前还是之后。

此外,默认情况下,每个没有相应 INSTANTIATE_TEST_SUITE_PTEST_P 都会在测试套件 GoogleTestVerification 中导致一个失败的测试。如果你有一个测试套件,其中这种遗漏不是错误(例如,它可能因其他原因而被链接,或者测试用例列表是动态的,可能为空),那么可以通过标记测试套件来抑制此检查:

GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(FooTest);

你可以查看 sample7_unittest.cc 和 sample8_unittest.cc 以获取更多示例。

12.2 创建值参数化抽象测试

在上述示例中,我们在 同一个 源文件中定义和实例化了 FooTest。有时,你可能希望在库中定义值参数化测试,然后让其他人稍后实例化它们。这种模式被称为 抽象测试。例如,当你设计一个接口时,你可以编写一个标准的抽象测试套件(可能使用工厂函数作为测试参数),所有接口的实现都期望通过这些测试。当有人实现接口时,他们可以实例化你的套件,以免费获得所有接口一致性测试。

要定义抽象测试,你应该这样组织你的代码:

  1. 将参数化测试夹具类的定义(例如 FooTest)放在一个头文件中,比如 foo_param_test.h。可以将其视为 声明 你的抽象测试。
  2. TEST_P 定义放在 foo_param_test.cc 中,它包含 foo_param_test.h。可以将其视为 实现 你的抽象测试。

一旦定义好了,你可以通过包含 foo_param_test.h,调用 INSTANTIATE_TEST_SUITE_P(),并依赖包含 foo_param_test.cc 的库目标来实例化它们。你可以在不同的源文件中多次实例化同一个抽象测试套件,甚至可以多次在同一个源文件中实例化。

12.3 为值参数化测试参数指定名称

INSTANTIATE_TEST_SUITE_P() 的可选最后一个参数允许用户基于测试参数生成自定义的测试名称后缀。该函数应该接受一个参数类型为 testing::TestParamInfo<class ParamType> 的参数,并返回 std::string

testing::PrintToStringParamName 是一个内置的测试后缀生成器,它返回 testing::PrintToString(GetParam()) 的值。它不适用于 std::string 或 C 字符串。

注意:测试名称必须是非空的、唯一的,并且只能包含 ASCII 字母数字字符。特别是,它们不应该包含下划线。

class MyTestSuite : public testing::TestWithParam<int> {};

TEST_P(MyTestSuite, MyTest)
{
  std::cout << "Example Test Param: " << GetParam() << std::endl;
}

INSTANTIATE_TEST_SUITE_P(
    MyGroup, MyTestSuite, testing::Range(0, 10),
    testing::PrintToStringParamName());

提供自定义函数允许你对测试参数名称生成有更多的控制权,特别是对于那些自动转换生成的参数名称没有帮助的类型(例如上面的字符串)。以下示例说明了如何为多个参数、枚举类型和字符串这样做,并演示了如何结合生成器。它使用 lambda 以简洁的方式表示:

enum class MyType { MY_FOO = 0, MY_BAR = 1 };

class MyTestSuite : public testing::TestWithParam<std::tuple<MyType, std::string>> {
};

INSTANTIATE_TEST_SUITE_P(
    MyGroup, MyTestSuite,
    testing::Combine(
        testing::Values(MyType::MY_FOO, MyType::MY_BAR),
        testing::Values("A", "B")),
    [](const testing::TestParamInfo<MyTestSuite::ParamType>& info) {
      std::string name = absl::StrCat(
          std::get<0>(info.param) == MyType::MY_FOO ? "Foo" : "Bar",
          std::get<1>(info.param));
      absl::c_replace_if(name, [](char c) { return !std::isalnum(c); }, '_');
      return name;
    });

13 类型化测试

假设你有同一个接口的多个实现,并希望确保它们都满足一些共同的要求。或者,你可能定义了几个应该符合同一“概念”的类型,并希望验证这一点。在这两种情况下,你都希望对不同的类型重复相同的测试逻辑。

虽然你可以为每个要测试的类型编写一个 TESTTEST_F(你甚至可以编写一个函数模板来调用这些 TEST),但这很繁琐,且无法扩展:如果你想要对 m 个测试进行 n 个类型的测试,你最终会编写 m*nTEST

类型化测试 允许你对类型列表重复相同的测试逻辑。你只需要编写一次测试逻辑,尽管你必须在编写类型化测试时知道类型列表。以下是操作方法:

首先,定义一个带参数的夹具类模板。记得让它从 ::testing::Test 派生:

template <typename T>
class FooTest : public testing::Test {
 public:
  ...
  using List = std::list<T>;
  static T shared_;
  T value_;
};

接下来,将类型列表与测试套件关联起来,这将为列表中的每个类型重复执行:

using MyTypes = ::testing::Types<char, int, unsigned int>;
TYPED_TEST_SUITE(FooTest, MyTypes);

类型别名(usingtypedef)是必要的,以便 TYPED_TEST_SUITE 宏正确解析。否则,编译器会认为类型列表中的每个逗号都引入了一个新的宏参数。

然后,使用 TYPED_TEST() 而不是 TEST_F() 来为此测试套件定义一个类型化测试。你可以根据需要重复此操作多次:

TYPED_TEST(FooTest, DoesBlah) {
  // 在测试中,通过访问 `TestFixture::` 前缀来访问夹具类的成员。
  TypeParam n = this->value_;

  // 要访问夹具类的静态成员,添加 `TestFixture::` 前缀。
  n += TestFixture::shared_;

  // 要引用夹具类中的 typedef,添加 `typename TestFixture::` 前缀。`typename` 是必须的,以满足编译器。
  typename TestFixture::List values;

  values.push_back(n);
  ...
}

TYPED_TEST(FooTest, HasPropertyA) { ... }

你可以查看 sample6_unittest.cc 以获取完整示例。

14 类型参数化测试

类型参数化测试与类型化测试类似,不同之处在于它们不需要你在编写测试逻辑之前知道类型列表。相反,你可以先定义测试逻辑,然后稍后使用不同的类型列表来实例化它。你甚至可以在同一个程序中多次实例化它。

如果你正在设计一个接口或概念,你可以定义一个类型参数化测试套件,以验证任何有效实现的接口/概念应该具有的属性。然后,每个实现的作者只需使用他们的类型实例化测试套件,以验证它是否符合要求,而无需反复编写类似的测试。这里有一个例子:

首先,定义一个带参数的夹具类模板,就像在类型化测试中所做的那样:

template <typename T>
class FooTest : public testing::Test {
  void DoSomethingInteresting();
  ...
};

接下来,声明你将定义一个类型参数化测试套件:

TYPED_TEST_SUITE_P(FooTest);

然后,使用 TYPED_TEST_P() 定义一个类型参数化测试。你可以根据需要重复此操作多次:

TYPED_TEST_P(FooTest, DoesBlah) {
  // 在测试中,通过引用 TypeParam 来获取类型参数。
  TypeParam n = 0;

  // 你可能需要显式使用 `this` 来引用夹具成员。
  this->DoSomethingInteresting();
  ...
}

TYPED_TEST_P(FooTest, HasPropertyA) { ... }

现在,你需要使用 REGISTER_TYPED_TEST_SUITE_P 宏注册所有测试模式,然后才能实例化它们。宏的第一个参数是测试套件名称;其余参数是此测试套件中的测试名称:

REGISTER_TYPED_TEST_SUITE_P(FooTest,
                            DoesBlah, HasPropertyA);

最后,你可以自由地使用你想要的类型来实例化该模式。如果你将上述代码放在一个头文件中,你可以通过 #include 它在多个 C++ 源文件中,并多次实例化它。

using MyTypes = ::testing::Types<char, int, unsigned int>;
INSTANTIATE_TYPED_TEST_SUITE_P(My, FooTest, MyTypes);

为了区分不同实例的模式,INSTANTIATE_TYPED_TEST_SUITE_P 宏的第一个参数是一个前缀,它将被添加到实际测试套件名称中。记得为不同的实例选择唯一的前缀。

在特殊情况下,如果类型列表只包含一个类型,你可以直接写那个类型,而不用 ::testing::Types<...>,如下所示:

INSTANTIATE_TYPED_TEST_SUITE_P(My, FooTest, int);

你可以查看 sample6_unittest.cc 以获取完整示例。

15 测试私有代码

如果你更改了软件的内部实现,只要更改对用户不可见,你的测试就不应该失败。因此,根据黑盒测试原则,大多数时候你应该通过其公共接口来测试代码。

如果你仍然发现自己需要测试内部实现代码,请考虑是否有更好的设计。 想要测试内部实现通常是类做太多事情的迹象。考虑将实现类提取出来,并测试它。然后在原始类中使用该实现类。

如果你确实必须测试非公共接口代码,你可以。这里有两种情况需要考虑:

15.1 静态函数或匿名命名空间中的定义/声明

无论是静态函数还是匿名命名空间中的定义/声明,都只在同一个翻译单元中可见。要测试它们,将私有代码移动到 foo::internal 命名空间中,其中 foo 是你的项目通常使用的命名空间,并将私有声明放在 *-internal.h 文件中。你的生产 .cc 文件和你的测试被允许包含这个内部头文件,但你的客户不能。这样,你可以完全测试你的内部实现,而不会泄露给你的客户。

注意:实际上也可以通过在 *_test.cc 文件中 #include 被测试的整个 .cc 文件来测试静态函数和匿名命名空间中的定义/声明。然而,这种技术不被本文档推荐,这里只是出于完整性而提及。

15.2 私有或受保护的类成员

要访问类的私有成员,你可以声明你的测试夹具类为该类的朋友,并在夹具中定义访问器。使用夹具的测试可以通过夹具中的访问器访问你生产类的私有成员。请注意,尽管你的夹具是生产类的朋友,但你的测试并不是自动成为它的朋友,因为它们在技术上是夹具的子类。

另一种测试私有成员的方法是将它们重构为一个实现类,该类然后在 *-internal.h 文件中声明。你的客户不允许包含这个头文件,但你的测试可以。

或者,你可以通过在类体中添加以下行来声明一个单独的测试作为类的朋友:

FRIEND_TEST(TestSuiteName, TestName);

例如:

// foo.h
class Foo {
    ...
private:
    FRIEND_TEST(FooTest, BarReturnsZeroOnNull);

    int Bar(void* x);
};

// foo_test.cc
...
TEST(FooTest, BarReturnsZeroOnNull) {
    Foo foo;
    EXPECT_EQ(foo.Bar(NULL), 0);  // 使用 Foo 的私有成员 Bar()。
}

请注意,当你在命名空间中定义类时,如果你希望你的测试夹具和测试成为类的朋友,那么它们必须定义在完全相同的命名空间中(没有匿名或内联命名空间)。

例如,如果要测试的代码看起来像:

namespace my_namespace {

class Foo {
    friend class FooTest;
    FRIEND_TEST(FooTest, Bar);
    FRIEND_TEST(FooTest, Baz);
    ... 类 Foo 的定义 ...
};

}  // namespace my_namespace

你的测试代码应该是这样的:

namespace my_namespace {

class FooTest : public testing::Test {
protected:
    ...
};

TEST_F(FooTest, Bar) { ... }
TEST_F(FooTest, Baz) { ... }

}  // namespace my_namespace

16 “捕获”失败

如果你正在构建一个基于 GoogleTest 的测试工具,你将想要测试这个工具。挑战在于验证你的测试工具是否正确报告失败。

"gtest/gtest-spi.h" 包含了一些用于实现这一点的构造。在包含这个头文件之后,你可以使用

EXPECT_FATAL_FAILURE(statement, substring);

来断言 statement 在当前线程中生成一个致命的(例如 ASSERT_*)失败,其消息包含给定的 substring,或者使用

EXPECT_NONFATAL_FAILURE(statement, substring);

如果你期望一个非致命的(例如 EXPECT_*)失败。

只有当前线程中的失败才会被检查以确定这种类型断言的结果。如果 statement 创建了新线程,这些线程中的失败也会被忽略。如果你还希望捕获其他线程中的失败,请改用以下宏之一:

EXPECT_FATAL_FAILURE_ON_ALL_THREADS(statement, substring);
EXPECT_NONFATAL_FAILURE_ON_ALL_THREADS(statement, substring);

注意:你不能将失败消息流式传输到这些宏中的任何一个。

  • statementEXPECT_FATAL_FAILURE{_ON_ALL_THREADS}() 中不能引用局部非静态变量或 this 对象的非静态成员。

  • statementEXPECT_FATAL_FAILURE{_ON_ALL_THREADS}() 中不能返回值。

17 动态注册测试

TEST 宏涵盖了几乎所有用例,但在某些情况下,需要运行时注册逻辑。对于这些情况,框架提供了 ::testing::RegisterTest,它允许调用者动态注册任意测试。

这是一个仅在 TEST 宏不足以使用时才使用的高级 API。应尽可能优先使用宏,因为它们避免了调用此函数的大部分复杂性。

它提供了以下签名:

template <typename Factory>
TestInfo* RegisterTest(const char* test_suite_name, const char* test_name,
                       const char* type_param, const char* value_param,
                       const char* file, int line, Factory factory);

factory 参数是一个可调用的工厂对象或函数指针,用于创建测试对象的新实例。它负责将所有权传递给调用者。可调用的签名是 Fixture*(),其中 Fixture 是测试的夹具类。使用相同 test_suite_name 注册的所有测试必须返回相同的夹具类型。这在运行时进行检查。

框架将从工厂中推断夹具类,并将为它调用 SetUpTestSuiteTearDownTestSuite

必须在调用 RUN_ALL_TESTS() 之前调用它,否则行为未定义。

用法示例:

class MyFixture : public testing::Test {
 public:
  // 这些都是可选的,就像在常规宏使用中一样。
  static void SetUpTestSuite() { ... }
  static void TearDownTestSuite() { ... }
  void SetUp() override { ... }
  void TearDown() override { ... }
};

class MyTest : public MyFixture {
 public:
  explicit MyTest(int data) : data_(data) {}
  void TestBody() override { ... }

 private:
  int data_;
};

void RegisterMyTests(const std::vector<int>& values) {
  for (int v : values) {
    testing::RegisterTest(
        "MyFixture", ("Test" + std::to_string(v)).c_str(), nullptr,
        std::to_string(v).c_str(),
        __FILE__, __LINE__,
        // 重要的是要使用夹具类型作为返回类型。
        [=]() -> MyFixture* { return new MyTest(v); });
  }
}
...
int main(int argc, char** argv) {
  testing::InitGoogleTest(&argc, argv);
  std::vector<int> values_to_test = LoadValuesFromConfig();
  RegisterMyTests(values_to_test);
  ...
  return RUN_ALL_TESTS();
}

18 获取当前测试的名称

有时一个函数可能需要知道当前正在运行的测试的名称。例如,你可能在测试夹具的 SetUp() 方法中根据正在运行的测试来设置黄金文件名。TestInfo 类包含这些信息。

要获取当前正在运行的测试的 TestInfo 对象,请在 UnitTest 单例对象上调用 current_test_info()

  // 获取有关当前正在运行的测试的信息。
  // 不要删除返回的对象——它由 UnitTest 类管理。
  const testing::TestInfo* const test_info =
      testing::UnitTest::GetInstance()->current_test_info();

  printf("我们正在测试 %s.%s 中的测试。\n",
         test_info->test_suite_name(),
         test_info->name());

current_test_info() 在没有测试运行时返回一个空指针。特别是,你不能在 SetUpTestSuite()TearDownTestSuite()(在其中你隐式地知道测试套件名称)或从它们调用的函数中找到测试套件名称。

19 通过处理测试事件扩展 GoogleTest

GoogleTest 提供了一个 事件监听器 API,以便你可以接收关于测试程序进度和测试失败的通知。你可以监听的事件包括测试程序、测试套件或测试方法的开始和结束等。你可以使用此 API 来增强或替换标准控制台输出、替换 XML 输出,或者提供一种完全不同的输出形式,例如 GUI 或数据库。你还可以使用测试事件作为检查点来实现资源泄漏检查器,例如。

19.1 定义事件监听器

要定义一个事件监听器,你需要从 testing::TestEventListenertesting::EmptyTestEventListener 派生。前者是一个(抽象)接口,其中 每个纯虚函数都可以被覆盖以处理测试事件(例如,当测试开始时,将调用 OnTestStart() 方法)。后者为接口中的所有方法提供了一个空实现,因此子类只需要覆盖它关心的方法。

当触发事件时,其上下文将作为参数传递给处理函数。以下参数类型被使用:

  • UnitTest 反映整个测试程序的状态,
  • TestSuite 包含有关测试套件的信息,它可以包含一个或多个测试,
  • TestInfo 包含测试的状态,以及
  • TestPartResult 表示测试断言的结果。

事件处理函数可以检查接收到的参数以了解有关事件和测试程序状态的有趣信息。

以下是一个示例:

  class MinimalistPrinter : public testing::EmptyTestEventListener {
    // 在测试开始时调用。
    void OnTestStart(const testing::TestInfo& test_info) override {
      printf("*** 测试 %s.%s 正在开始。\n",
             test_info.test_suite_name(), test_info.name());
    }

    // 在失败的断言或 `SUCCESS()` 之后调用。
    void OnTestPartResult(const testing::TestPartResult& test_part_result) override {
      printf("%s 在 %s:%d\n%s\n",
             test_part_result.failed() ? "*** 失败" : "成功",
             test_part_result.file_name(),
             test_part_result.line_number(),
             test_part_result.summary());
    }

    // 在测试结束时调用。
    void OnTestEnd(const testing::TestInfo& test_info) override {
      printf("*** 测试 %s.%s 正在结束。\n",
             test_info.test_suite_name(), test_info.name());
    }
  };

19.2 使用事件监听器

要使用你定义的事件监听器,请在 main() 函数中,在调用 RUN_ALL_TESTS() 之前,将它的一个实例添加到 GoogleTest 事件监听器列表(由类 TestEventListeners 表示——注意名称末尾的“s”)中:

int main(int argc, char** argv) {
  testing::InitGoogleTest(&argc, argv);
  // 获取事件监听器列表。
  testing::TestEventListeners& listeners =
      testing::UnitTest::GetInstance()->listeners();
  // 在末尾添加一个监听器。GoogleTest 拥有它。
  listeners.Append(new MinimalistPrinter);
  return RUN_ALL_TESTS();
}

只有一个问题:默认的测试结果打印机仍然有效,因此它的输出将与你的极简打印机的输出混在一起。要禁用默认打印机,只需从事件监听器列表中释放它并删除它。你可以通过添加以下一行来实现:

  ...
  delete listeners.Release(listeners.default_result_printer());
  listeners.Append(new MinimalistPrinter);
  return RUN_ALL_TESTS();

现在,坐下来享受你的测试完全不同的输出吧。更多详细信息,请参阅 sample9_unittest.cc。

你可以将多个监听器添加到列表中。当触发 On*Start()OnTestPartResult() 事件时,监听器将按它们在列表中出现的顺序接收它(由于新监听器被添加到列表末尾,因此默认文本打印机和默认 XML 生成器将首先接收事件)。On*End() 事件将按 相反 的顺序被监听器接收。这允许稍后添加的监听器的输出被较早添加的监听器的输出所包围。

19.3 在监听器中生成失败

你可以在处理事件时使用失败引发宏(EXPECT_*()ASSERT_*()FAIL() 等)。有一些限制:

  1. 你不能在 OnTestPartResult() 中生成任何失败(否则它将导致 OnTestPartResult() 递归调用)。
  2. 处理 OnTestPartResult() 的监听器不允许生成任何失败。

当你将监听器添加到监听器列表时,你应该将处理 OnTestPartResult() 的监听器放在 生成失败的监听器之前。这确保了由后者生成的失败被前者正确归因于正确的测试。

请参阅 sample10_unittest.cc 以获取一个引发失败的监听器示例。

20 运行测试程序:高级选项

GoogleTest 测试程序是普通的可执行文件。一旦构建完成,你可以直接运行它们,并通过以下环境变量和/或命令行标志影响它们的行为。为了使标志生效,你的程序必须在调用 RUN_ALL_TESTS() 之前调用 ::testing::InitGoogleTest()

要查看支持的标志列表及其用法,请运行你的测试程序并带有 --help 标志。

如果一个选项既通过环境变量又通过标志指定,则后者优先。

20.1 选择测试

20.1.1 列出测试名称

有时,在运行测试之前,有必要列出程序中可用的测试,以便在需要时应用过滤器。包含标志 --gtest_list_tests 将覆盖所有其他标志,并以以下格式列出测试:

TestSuite1.
  TestName1
  TestName2
TestSuite2.
  TestName

如果提供了该标志,则不会实际运行列出的任何测试。此标志没有对应的环境变量。

20.1.2 运行测试子集

默认情况下,GoogleTest 程序会运行用户定义的所有测试。有时,你可能希望仅运行测试的一个子集(例如,用于调试或快速验证更改)。如果你将 GTEST_FILTER 环境变量或 --gtest_filter 标志设置为过滤字符串,GoogleTest 将仅运行其完整名称(形式为 TestSuiteName.TestName)与过滤器匹配的测试。

过滤器的格式是一个以 : 分隔的通配符模式列表(称为 正模式),可选地后面跟着一个 - 和另一个以 : 分隔的模式列表(称为 负模式)。当且仅当测试匹配任何正模式但不匹配任何负模式时,测试才匹配过滤器。

模式可以包含 '*'(匹配任何字符串)或 '?'(匹配任何单个字符)。为了方便起见,过滤器 '*-NegativePatterns' 也可以写成 '-NegativePatterns'

例如:

  • ./foo_test 没有标志,因此运行所有测试。
  • ./foo_test --gtest_filter=* 也有一个匹配所有内容的单个 * 值,因此运行所有内容。
  • ./foo_test --gtest_filter=FooTest.* 运行测试套件 FooTest 中的所有内容。
  • ./foo_test --gtest_filter=*Null*:*Constructor* 运行完整名称中包含 "Null""Constructor" 的任何测试。
  • ./foo_test --gtest_filter=-*DeathTest.* 运行所有非死亡测试。
  • ./foo_test --gtest_filter=FooTest.*-FooTest.Bar 运行测试套件 FooTest 中的所有内容,除了 FooTest.Bar
  • ./foo_test --gtest_filter=FooTest.*:BarTest.*-FooTest.Bar:BarTest.Foo 运行测试套件 FooTest 中的所有内容,除了 FooTest.Bar,以及测试套件 BarTest 中的所有内容,除了 BarTest.Foo

20.1.3 在首次失败时停止测试执行

默认情况下,GoogleTest 程序会运行用户定义的所有测试。在某些情况下(例如,迭代测试开发和执行),可能希望在首次失败时停止测试执行(以提高速度为代价换取完整性)。如果设置了 GTEST_FAIL_FAST 环境变量或 --gtest_fail_fast 标志,测试运行器将在找到第一个测试失败时停止执行。

20.1.4 暂时禁用测试

如果你有一个无法立即修复的失败测试,你可以通过在名称前添加 DISABLED_ 前缀来禁用它。这将从执行中排除它。这比注释掉代码或使用 #if 0 更好,因为禁用的测试仍然会被编译(因此不会腐烂)。

如果你想禁用一个测试套件中的所有测试,你可以分别在每个测试名称前添加 DISABLED_,或者在测试套件名称前添加它。

例如,以下测试不会被 GoogleTest 运行,尽管它们仍然会被编译:

// 测试 Foo 是否执行 Abc。
TEST(FooTest, DISABLED_DoesAbc) { ... }

class DISABLED_BarTest : public testing::Test { ... };

// 测试 Bar 是否执行 Xyz。
TEST_F(DISABLED_BarTest, DoesXyz) { ... }

注意:此功能仅应用于临时缓解痛苦。你仍然需要在稍后修复禁用的测试。作为一个提醒,GoogleTest 将打印一个横幅,警告你如果一个测试程序包含任何禁用的测试。

提示:你可以轻松地使用 grep 统计你拥有的禁用测试数量。这个数字可以作为改进测试质量的指标。

20.1.5 暂时启用禁用的测试

要将禁用的测试包含在测试执行中,只需带有 --gtest_also_run_disabled_tests 标志运行测试程序,或者将 GTEST_ALSO_RUN_DISABLED_TESTS 环境变量设置为非 0 的值。你可以将此与 --gtest_filter 标志结合使用,以进一步选择要运行的禁用测试。

20.2 强制至少有一个测试用例

程序员的一个常见错误是编写了一个没有链接测试用例的测试程序。这可能发生在例如,你将测试用例定义在一个库中,而该库没有被标记为“始终链接”。

为了捕获这种错误,带有 --gtest_fail_if_no_test_linked 标志运行测试程序,或者将 GTEST_FAIL_IF_NO_TEST_LINKED 环境变量设置为非 0 的值。现在,如果测试程序中没有链接测试用例,程序将失败。

请注意,任何 链接的测试用例都使程序对于此检查有效。特别是,即使被禁用的测试用例也足够。

20.3 强制至少运行一个测试用例

除了使用 --gtest_fail_if_no_test_linked 强制定义测试用例外,还可以使用 --gtest_fail_if_no_test_selected 强制实际运行一个测试用例,以确保不会消耗不做任何事情的测试资源。

要捕获这种优化机会,请带有 --gtest_fail_if_no_test_selected 标志运行测试程序,或者将 GTEST_FAIL_IF_NO_TEST_SELECTED 环境变量设置为非 0 的值。

如果开始运行测试用例,即使它稍后通过 GTEST_SKIP 被跳过,该测试用例也被视为已选择。因此,DISABLED 测试用例不被视为已选择,--gtest_filter 不匹配的测试用例也不被视为已选择。

20.4 重复测试

偶尔你会遇到一个测试结果是随机的。也许它只有 1% 的时间会失败,这使得在调试器下重现错误变得相当困难。这种测试的随机性是一个主要的挫折来源。

--gtest_repeat 标志允许你重复程序中的所有(或选定的)测试方法多次。希望,一个随机的测试最终会失败,给你一个调试的机会。以下是使用方法:

$ foo_test --gtest_repeat=1000
重复 foo_test 1000 次,不会在失败时停止。

$ foo_test --gtest_repeat=-1
负数表示无限重复。

$ foo_test --gtest_repeat=1000 --gtest_break_on_failure
重复 foo_test 1000 次,在第一次失败时停止。这在调试器下特别有用:当测试失败时,它将进入调试器,然后你可以检查变量和堆栈。

$ foo_test --gtest_repeat=1000 --gtest_filter=FooBar.*
重复匹配过滤器的测试 1000 次。

如果你的测试程序包含全局设置/拆除代码,它也会在每次迭代中重复执行,因为随机性可能存在于其中。为了避免在每次迭代中重复全局设置/拆除,请指定 --gtest_recreate_environments_when_repeating=false

你也可以通过设置 GTEST_REPEAT 环境变量来指定重复次数。

20.5 打乱测试顺序

你可以指定 --gtest_shuffle 标志(或者将 GTEST_SHUFFLE 环境变量设置为 1),以随机顺序运行程序中的测试。这有助于揭示测试之间的不良依赖关系。

默认情况下,GoogleTest 使用从当前时间计算出的随机种子。因此你每次都会得到不同的顺序。控制台输出包括随机种子值,这样你就可以在稍后重现与顺序相关的测试失败。要显式指定随机种子,请使用 --gtest_random_seed=SEED 标志(或者设置 GTEST_RANDOM_SEED 环境变量),其中 SEED 是一个介于 [0, 99999] 的整数。种子值 0 是特殊的:它告诉 GoogleTest 执行默认行为,即从当前时间计算种子。

如果你将此与 --gtest_repeat=N 结合使用,GoogleTest 将在每次迭代中选择一个不同的随机种子并重新打乱测试。

20.6 将测试函数分配到多台机器上

如果你有多台机器可以用来运行测试程序,你可能希望并行运行测试函数,以更快地获得结果。我们将这种技术称为 分片,其中每台机器称为一个 分片。

GoogleTest 与测试分片兼容。要利用此功能,你的测试运行器(不是 GoogleTest 的一部分)需要执行以下操作:

  1. 分配多台机器(分片)来运行测试。
  2. 在每台机器上,设置 GTEST_TOTAL_SHARDS 环境变量为分片总数。它必须对所有分片都相同。
  3. 在每台机器上,设置 GTEST_SHARD_INDEX 环境变量为分片的索引。不同的分片必须被分配不同的索引,这些索引必须在范围 [0, GTEST_TOTAL_SHARDS - 1] 内。
  4. 在所有分片上运行相同的测试程序。当 GoogleTest 看到上述两个环境变量时,它将选择一组测试函数来运行。在所有分片中,程序中的每个测试函数将被运行一次。
  5. 等待所有分片完成,然后收集并报告结果。

你的项目可能有一些未使用 GoogleTest 编写的测试,因此它们不理解这个协议。为了让你的测试运行器能够确定哪个测试支持分片,它可以设置环境变量 GTEST_SHARD_STATUS_FILE 为一个不存在的文件路径。如果一个测试程序支持分片,它将创建这个文件以确认这一事实;否则它不会创建它。目前,该文件的实际内容并不重要,虽然我们可能会在将来在其中放入一些有用的信息。

以下是一个示例,以使其清晰。假设你有一个测试程序 foo_test,它包含以下 5 个测试函数:

TEST(A, V)
TEST(A, W)
TEST(B, X)
TEST(B, Y)
TEST(B, Z)

假设你有 3 台机器可供使用。要并行运行测试函数,你将在所有机器上设置 GTEST_TOTAL_SHARDS 为 3,并分别在机器上设置 GTEST_SHARD_INDEX 为 0、1 和 2。然后你会在每台机器上运行相同的 foo_test

GoogleTest 保留了如何在分片之间分配工作的方式的权利,但这里有一个可能的场景:

  • 机器 #0 运行 A.VB.X
  • 机器 #1 运行 A.WB.Y
  • 机器 #2 运行 B.Z

20.7 控制测试输出

20.7.1 彩色终端输出

GoogleTest 可以在终端输出中使用颜色,使重要信息更容易被发现:

...
[----------] 1 测试来自 FooTest
[ RUN      ] FooTest.DoesAbc
[       OK ] FooTest.DoesAbc
[----------] 2 测试来自 BarTest
[ RUN      ] BarTest.HasXyzProperty
[       OK ] BarTest.HasXyzProperty
[ RUN      ] BarTest.ReturnsTrueOnSuccess
... 一些错误消息 ...
[   FAILED ] BarTest.ReturnsTrueOnSuccess
...
[==========] 30 测试来自 14 个测试套件
[   PASSED ] 28 测试
[   FAILED ] 2 测试,列表如下:
[   FAILED ] BarTest.ReturnsTrueOnSuccess
[   FAILED ] AnotherTest.DoesXyz

 2 FAILED TESTS

你可以通过设置 GTEST_COLOR 环境变量或 --gtest_color 命令行标志为 yesnoauto(默认值)来启用颜色、禁用颜色或让 GoogleTest 自行决定。当值为 auto 时,GoogleTest 将仅在输出到终端且(在非 Windows 平台上)TERM 环境变量设置为 xtermxterm-color 时使用颜色。

20.7.2 抑制测试通过

默认情况下,GoogleTest 为每个测试打印一行输出,指示它是否通过或失败。要仅显示测试失败,请带有 --gtest_brief=1 标志运行测试程序,或者将 GTEST_BRIEF 环境变量设置为 1

20.7.3 抑制经过时间

默认情况下,GoogleTest 打印每个测试运行所需的时间。要禁用此功能,请带有 --gtest_print_time=0 命令行标志运行测试程序,或者将 GTEST_PRINT_TIME 环境变量设置为 0

20.7.4 抑制 UTF-8 文本输出

在断言失败的情况下,GoogleTest 会以十六进制编码字符串的形式以及可读的 UTF-8 文本(如果它们包含有效的非 ASCII UTF-8 字符)打印类型为 string 的预期值和实际值。如果你想因为例如输出媒介不支持 UTF-8 而抑制 UTF-8 文本,请带有 --gtest_print_utf8=0 标志运行测试程序,或者将 GTEST_PRINT_UTF8 环境变量设置为 0

20.7.5 生成 XML 报告

GoogleTest 可以生成一个详细的 XML 报告,除了正常的文本输出外。报告包含每个测试的持续时间,因此可以帮助你识别慢速测试。

要生成 XML 报告,请将 GTEST_OUTPUT 环境变量或 --gtest_output 标志设置为字符串 "xml:path_to_output_file",这将在指定位置创建文件。你也可以只使用字符串 "xml",在这种情况下,输出可以在当前目录中的 test_detail.xml 文件中找到。

如果你指定一个目录(例如,在 Linux 上 "xml:output/directory/" 或在 Windows 上 "xml:output\directory\"),GoogleTest 将在该目录中创建 XML 文件,文件名以测试可执行文件命名(例如,测试程序为 foo_testfoo_test.exe 时,文件名为 foo_test.xml)。如果文件已经存在(可能是之前运行遗留下来的),GoogleTest 将选择不同的名称(例如 foo_test_1.xml)以避免覆盖它。

报告基于 junitreport Ant 任务。由于该格式最初是为 Java 设计的,因此需要进行一些解释以使其适用于 GoogleTest 测试,如下所示:

<testsuites name="AllTests" ...>
  <testsuite name="test_case_name" ...>
    <testcase    name="test_name" ...>
      <failure message="..."/>
      <failure message="..."/>
      <failure message="..."/>
    </testcase>
  </testsuite>
</testsuites>
  • <testsuites> 元素对应于整个测试程序。
  • <testsuite> 元素对应于 GoogleTest 测试套件。
  • <testcase> 元素对应于 GoogleTest 测试函数。

例如,以下程序

TEST(MathTest, Addition) { ... }
TEST(MathTest, Subtraction) { ... }
TEST(LogicTest, NonContradiction) { ... }

可能会生成以下报告:

<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="3" failures="1" errors="0" time="0.035" timestamp="2011-10-31T18:52:42" name="AllTests">
  <testsuite name="MathTest" tests="2" failures="1" errors="0" time="0.015">
    <testcase name="Addition" file="test.cpp" line="1" status="run" time="0.007" classname="">
      <failure message="Value of: add(1, 1)&#x0A;  Actual: 3&#x0A;Expected: 2" type="">...</failure>
      <failure message="Value of: add(1, -1)&#x0A;  Actual: 1&#x0A;Expected: 0" type="">...</failure>
    </testcase>
    <testcase name="Subtraction" file="test.cpp" line="2" status="run" time="0.005" classname="">
    </testcase>
  </testsuite>
  <testsuite name="LogicTest" tests="1" failures="0" errors="0" time="0.005">
    <testcase name="NonContradiction" file="test.cpp" line="3" status="run" time="0.005" classname="">
    </testcase>
  </testsuite>
</testsuites>

需要注意的事项:

  • <testsuites><testsuite> 元素的 tests 属性告诉 GoogleTest 程序或测试套件包含多少个测试函数,而 failures 属性告诉其中有多少个失败。

  • time 属性表示测试、测试套件或整个测试程序的持续时间,以秒为单位。

  • timestamp 属性记录测试执行的本地日期和时间。

  • fileline 属性记录测试定义所在的源文件位置。

  • 每个 <failure> 元素对应于一个单独的失败的 GoogleTest 断言。

20.7.6 生成 JSON 报告

GoogleTest 也可以发出 JSON 报告,作为 XML 的替代格式。要生成 JSON 报告,请将 GTEST_OUTPUT 环境变量或 --gtest_output 标志设置为字符串 "json:path_to_output_file",这将在指定位置创建文件。你也可以只使用字符串 "json",在这种情况下,输出可以在当前目录中的 test_detail.json 文件中找到。

报告格式符合以下 JSON Schema:

{
  "$schema": "https://json-schema.org/schema#",
  "type": "object",
  "definitions": {
    "TestCase": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "tests": { "type": "integer" },
        "failures": { "type": "integer" },
        "disabled": { "type": "integer" },
        "time": { "type": "string" },
        "testsuite": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/TestInfo"
          }
        }
      }
    },
    "TestInfo": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "file": { "type": "string" },
        "line": { "type": "integer" },
        "status": {
          "type": "string",
          "enum": ["RUN", "NOTRUN"]
        },
        "time": { "type": "string" },
        "classname": { "type": "string" },
        "failures": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/Failure"
          }
        }
      }
    },
    "Failure": {
      "type": "object",
      "properties": {
        "failures": { "type": "string" },
        "type": { "type": "string" }
      }
    }
  },
  "properties": {
    "tests": { "type": "integer" },
    "failures": { "type": "integer" },
    "disabled": { "type": "integer" },
    "errors": { "type": "integer" },
    "timestamp": {
      "type": "string",
      "format": "date-time"
    },
    "time": { "type": "string" },
    "name": { "type": "string" },
    "testsuites": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/TestCase"
      }
    }
  }
}

报告使用以下 Proto3 格式,使用 JSON 编码:

syntax = "proto3";

package googletest;

import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";

message UnitTest {
  int32 tests = 1;
  int32 failures = 2;
  int32 disabled = 3;
  int32 errors = 4;
  google.protobuf.Timestamp timestamp = 5;
  google.protobuf.Duration time = 6;
  string name = 7;
  repeated TestCase testsuites = 8;
}

message TestCase {
  string name = 1;
  int32 tests = 2;
  int32 failures = 3;
  int32 disabled = 4;
  int32 errors = 5;
  google.protobuf.Duration time = 6;
  repeated TestInfo testsuite = 7;
}

message TestInfo {
  string name
  string file = 6;
  int32 line = 7;
  enum Status {
    RUN = 0;
    NOTRUN = 1;
  }
  Status status = 2;
  google.protobuf.Duration time = 3;
  string classname = 4;
  message Failure {
    string failures = 1;
    string type = 2;
  }
  repeated Failure failures = 5;
}

例如,以下程序

TEST(MathTest, Addition) { ... }
TEST(MathTest, Subtraction) { ... }
TEST(LogicTest, NonContradiction) { ... }

可能会生成以下报告:

{
  "tests": 3,
  "failures": 1,
  "errors": 0,
  "time": "0.035s",
  "timestamp": "2011-10-31T18:52:42Z",
  "name": "AllTests",
  "testsuites": [
    {
      "name": "MathTest",
      "tests": 2,
      "failures": 1,
      "errors": 0,
      "time": "0.015s",
      "testsuite": [
        {
          "name": "Addition",
          "file": "test.cpp",
          "line": 1,
          "status": "RUN",
          "time": "0.007s",
          "classname": "",
          "failures": [
            {
              "message": "Value of: add(1, 1)\n  Actual: 3\nExpected: 2",
              "type": ""
            },
            {
              "message": "Value of: add(1, -1)\n  Actual: 1\nExpected: 0",
              "type": ""
            }
          ]
        },
        {
          "name": "Subtraction",
          "file": "test.cpp",
          "line": 2,
          "status": "RUN",
          "time": "0.005s",
          "classname": ""
        }
      ]
    },
    {
      "name": "LogicTest",
      "tests": 1,
      "failures": 0,
      "errors": 0,
      "time": "0.005s",
      "testsuite": [
        {
          "name": "NonContradiction",
          "file": "test.cpp",
          "line": 3,
          "status": "RUN",
          "time": "0.005s",
          "classname": ""
        }
      ]
    }
  ]
}

重要:JSON 文档的确切格式可能会发生变化。

20.7.7 检测测试提前退出

Google Test 实现了 premature-exit-file 协议,用于测试运行器捕获测试程序的任何意外退出。启动时,Google Test 创建一个文件,该文件将在所有工作完成后自动删除。然后,测试运行器可以检查该文件是否存在。如果文件仍然存在,则被检查的测试已提前退出。

仅当设置了 TEST_PREMATURE_EXIT_FILE 环境变量时,才会启用此功能。

20.7.8 将断言失败转换为断点

在调试器下运行测试程序时,如果调试器可以在断言失败时捕获异常并自动进入交互模式,这将非常方便。GoogleTest 的 断言失败时中断 模式支持这种行为。

要启用它,请将 GTEST_BREAK_ON_FAILURE 环境变量设置为非 0 的值。或者,你可以使用 --gtest_break_on_failure 命令行标志。

20.7.9 禁用捕获测试抛出的异常

GoogleTest 可以在启用或禁用异常的情况下使用。如果测试抛出 C++ 异常或(在 Windows 上)结构化异常(SEH),默认情况下 GoogleTest 会捕获它,将其报告为测试失败,并继续执行下一个测试方法。这最大化了测试运行的覆盖率。此外,在 Windows 上,未捕获的异常会导致弹出窗口,因此捕获异常允许你自动运行测试。

然而,在调试测试失败时,你可能希望由调试器处理异常,以便在抛出异常时检查调用栈。为此,请将 GTEST_CATCH_EXCEPTIONS 环境变量设置为 0,或者在运行测试时使用 --gtest_catch_exceptions=0 标志。

20.8 Sanitizer 集成

Undefined Behavior SanitizerAddress SanitizerThread Sanitizer 都提供了可以被覆盖以在检测到 Sanitizer 错误时触发显式失败的弱函数,例如从 nullptr 创建引用。要覆盖这些函数,请在编译为你的主二进制文件一部分的源文件中放置它们的定义:

extern "C" {
void __ubsan_on_report() {
  FAIL() << "Encountered an undefined behavior sanitizer error";
}
void __asan_on_error() {
  FAIL() << "Encountered an address sanitizer error";
}
void __tsan_on_report() {
  FAIL() << "Encountered a thread sanitizer error";
}
}  // extern "C"

启用 Sanitizer 后编译项目后,如果特定测试触发了 Sanitizer 错误,GoogleTest 将报告该测试失败。

未经允许不得转载:海淘实验室 » Google Test 高级主题

赞 (0)

评论 0