ktest 高级教程
介绍
现在您已经阅读了 kumotest Primer 并了解了如何使用 kumotest 编写测试,是时候学习一些新技巧了。本文件 将向您展示更多断言以及如何构建复杂的失败消息,传播致命故障,重用并加速您的测试装置,以及 在测试中使用各种标志。
更多断言
本节涵盖了一些不太常用但仍然很重要的内容,断言。
明确的成功和失败
请参阅显式成功和失败断言参考。
异常断言
请参阅断言中的异常断言参考。
谓词断言以获得更好的错误消息
尽管 kumotest 有一套丰富的断言,但它们永远不可能完整,因为不可能(也不是一个好主意)预测用户可能出现的所有场景
遇到。因此,有时用户必须使用EXPECT_TRUE()来检查复杂的表达,缺乏更好的宏。这有一个问题不
向您显示表达式各部分的值,使其很难了解出了什么问题。作为解决方法,一些用户选择构建
失败消息本身,将其流式传输到EXPECT_TRUE()。然而,这很尴尬,尤其是当表达式有副作用或成本高昂时
评价。
kumotest 为您提供了三种不同的选项来解决此问题:
使用现有的布尔函数
如果您已经有一个返回bool的函数或函子(或一个类型)可以隐式转换为 bool),您可以在 谓词中使用它
断言 免费打印函数参数。看断言中的 EXPECT_PRED*
详细参考。
使用返回 AssertionResult 的函数
虽然EXPECT_PRED*()和朋友可以方便地快速完成工作,但语法并不令人满意:您必须对不同的参数使用不同的宏,并且它
感觉更像 Lisp 而不是 C++。 ::testing::AssertionResult 类解决了
这个问题。
AssertionResult 对象表示断言的结果(无论是成功或失败,以及相 关消息)。您可以创建一个
AssertionResult 使用以下工厂函数之一:
namespace testing {
// Returns an AssertionResult object to indicate that an assertion has
// succeeded.
AssertionResult AssertionSuccess();
// Returns an AssertionResult object to indicate that an assertion has
// failed.
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 << " is odd";
}
而不是:
bool IsEven(int n) {
return (n % 2) == 0;
}
失败的断言EXPECT_TRUE(IsEven(Fib(4)))将打印:
Value of: IsEven(Fib(4))
Actual: false (3 is odd)
Expected: true
而不是更不透明的
Value of: IsEven(Fib(4))
Actual: false
Expected: true
如果您还想要EXPECT_FALSE和ASSERT_FALSE中的信息性消息
(Kumo 代码库中三分之一的布尔断言是负断言),以及
在成功的情况下可以让谓词变慢,你可以提供
成功消息:
testing::AssertionResult IsEven(int n) {
if ((n % 2) == 0)
return testing::AssertionSuccess() << n << " is even";
else
return testing::AssertionFailure() << n << " is odd";
}
然后将打印语句EXPECT_FALSE(IsEven(Fib(6)))
Value of: IsEven(Fib(6))
Actual: true (8 is even)
Expected: false
使用谓词格式化程序
如果您发现生成的默认消息
EXPECT_PRED* 和
EXPECT_TRUE 不满意,或者有些
您的谓词的参数不支持流式传输到ostream,您可以
相反,使用谓词格式化程序 断言来完全自定义
消息已格式化。看
EXPECT_PRED_FORMAT* 在
断言参考了解详细信息。
浮点比较
请参阅 浮点比较断言参考。
浮点谓词格式函数
一些浮点运算很有用,但不常用。为了避免新宏的爆炸,我们将它们作为谓词格式函数提供
可以在谓词断言宏中使用
EXPECT_PRED_FORMAT2,对于
例子:
using ::testing::FloatLE;
using ::testing::DoubleLE;
...
EXPECT_PRED_FORMAT2(FloatLE, val1, val2);
EXPECT_PRED_FORMAT2(DoubleLE, val1, val2);
上面的代码验证val1小于或大约等于val2。
使用 kMock 匹配器进行断言
请参阅断言中的 EXPECT_THAT
参考。
更多字符串断言
(请先阅读上一篇部分,如果你没有。)
您可以使用 kMock 字符串匹配器
使用 EXPECT_THAT 执行更多字符串
比较技巧(子字符串、前缀、后缀、正则表达式等)。为了例子,
using ::testing::HasSubstr;
using ::testing::MatchesRegex;
...
ASSERT_THAT(foo_string, HasSubstr("needle"));
EXPECT_THAT(bar_string, MatchesRegex("\\w*\\d+"));
Windows HRESULT 断言
请参阅 Windows HRESULT 断言断言参考。
类型断言
您 可以调用该函数
::testing::StaticAssertTypeEq<T1, T2>();
断言类型T1和T2是相同的。该函数不执行任何操作,如果断言得到满足。如果类型不同,函数调用将
编译失败,编译器错误信息会说T1和T2不是相同类型并且最有可能(取决于编译器)向您显示实际的
T1和T2的值。这主要在模板代码中有用。
警告:在类模板或函数的成员函数内部使用时
模板中, 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(); }
to cause a compiler error.
断言放置
您可以在任何 C++ 函数中使用断言。特别是,它不必是
测试夹具类的方法。一个限制是断言
生成致命故障(FAIL*和ASSERT_*)只能用于
返回 void 的函数。这是 Kumo 不使用的结果
例外情况。通过将其放在非 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)。你
需要确保“*结果”包含一些合理的值,即使
函数提前返回。由于该函数现在返回 void,您可以使用
其中的任何断言。
如果无法更改函数的类型,则应该使用断言产生非致命故障,例如ADD_FAILURE*和EXPECT_*。
注意:构造函数和析构函数不被视为返回 void 的函数,
根据C++语言规范,所以你不能使用 fatal
其中的断言;如果你尝试的话,你会得到一个编译错误。相反,要么
调用abort并使整个测试可执行文件崩溃,或者将致命断言放入
SetUp/TearDown 函数;看
构造函数/析构函数与 SetUp/TearDown
警告:辅助函数中的致命断言(私有 void 返回方法)
从构造函数或析构函数调用不会终止当前测试,因为
你的直觉可能会建议:它只是从构造函数返回或
尽早析构函数,可能会将您的对象留在部分构造或
部分破坏状态!您几乎肯定想要abort或使用
改为SetUp/TearDown。
跳过测试执行
与断言SUCCEED()和FAIL()相关,您可以阻止进一步的测试在运行时使用KTEST_SKIP()宏执行。当你需要时这很有用
在运行时检查被测系统的先决条件并跳过以有意义的方式进行测试。
KTEST_SKIP() 可以在单独的测试用例或 SetUp() 方法中使用从 ::testing::Environment 或 ::testing::Test 派生的类。
例如:
TEST(SkipTest, DoesSkip) {
KTEST_SKIP() << "Skipping single test";
EXPECT_EQ(0, 1); // Won't fail; it won't be executed
}
class SkipFixture : public ::testing::Test {
protected:
void SetUp() override {
KTEST_SKIP() << "Skipping all tests for this fixture";
}
};
// Tests for SkipFixture won't be executed.
TEST_F(SkipFixture, SkipsOneTest) {
EXPECT_EQ(5, 7); // Won't fail
}
与断言宏一样,您可以将自定义消息流式传输到KTEST_SKIP()中。
指导 kumotest 如何打印你的价值
当诸如EXPECT_EQ之类的测试断言失败时,kumotest 会打印参数值来帮助您调试。它使用用户可扩展的价值打印机来完成此操作。
该打印机知道如何打印内置 C++ 类型、本机数组、STL容器以及任何支持 << 运算符的类型。对于其他类型,它
打印值中的原始字节,并希望用户能够弄清楚。
如前所述,打印机是可扩展的。这意味着你可以教它比转储字节更好地打印您的特定类型。到为此,为您的类型定义<<:
#include <ostream>
namespace foo {
class Bar { // We want kumotest to be able to print instances of this.
...
// Create a free inline friend function.
friend std::ostream& operator<<(std::ostream& os, const Bar& bar) {
return os << bar.DebugString(); // whatever needed to print bar to os
}
};
// If you can't declare the function in the class it's important that the
// << operator is defined in the SAME namespace that defines Bar. C++'s look-up
// rules rely on that.
std::ostream& operator<<(std::ostream& os, const Bar& bar) {
return os << bar.DebugString(); // whatever needed to print bar to os
}
} // namespace foo
有时,这可能不是一个选择:您的团队可能认为这样做是不好的风格对于Bar有一个<<运算符,或者Bar可能已经有一个<<运算符
不做你想做的事(而且你无法改变它)。如果是这样,您可以改为定义一个 PrintTo() 函数,如下所示:
#include <ostream>
namespace foo {
class Bar {
...
friend void PrintTo(const Bar& bar, std::ostream* os) {
*os << bar.DebugString(); // whatever needed to print bar to os
}
};
// If you can't declare the function in the class it's important that PrintTo()
// is defined in the SAME namespace that defines Bar. C++'s look-up rules rely
// on that.
void PrintTo(const Bar& bar, std::ostream* os) {
*os << bar.DebugString(); // whatever needed to print bar to os
}
} // namespace foo
如果您同时定义了 << 和 PrintTo(),则在以下情况下将使用后者kumotest 很关心。这允许您自定义值的显示方式
kumotest 的输出不会影响依赖于其行为的代码<< 运算符。
如果您想自己使用 kumotest 的值打印机打印值x,只需调用::testing::PrintToString(x),它返回一个std::string:
vector<pair<Bar, int> > bar_ints = GetBarIntVector();
EXPECT_TRUE(IsCorrectBarIntVector(bar_ints))
<< "bar_ints = " << testing::PrintToString(bar_ints);
死亡测试
在许多应用程序中,如果存在断言,则可能会导致应用程序失败 不满足条件。这些一致性检查可确保程序 处于已知的良好状态,之后是否会尽早出现故障 某些程序状态已损坏。如果断言检查了错误的条件, 那么程序可能会在错误的状态下继续运行,这可能会导致内存 腐败、安全漏洞或更糟。因此,测试这一点至关重要 这样的断言语句按预期工作。
由于这些前提条件检查会导致进程终止,因此我们将此类测试称为 死亡测试。更一般地说,任何检查程序是否终止的测试 (除了抛出异常)以预期的方式也是一个死亡测试。
请注意,如果一段代码抛出异常,我们不认为它是“死亡” 出于死亡测试的目的,因为代码的调用者可以捕获 异常并避免崩溃。如果你想验证你的抛出的异常 代码,请参阅异常断言。
如果您想在测试代码中测试EXPECT_*()/ASSERT_*()失败,请参阅捕捉失败。
如何编写死亡测试
KumoTest 提供断言宏来支持死亡测试。看断言参考中的 死亡断言 了解详情。
要编写死亡测试,只需使用测试函数中的宏之一即可。 例 如,
TEST(MyDeathTest, Foo) {
// This death test uses a compound statement.
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终止进程。
测试函数体还可以包含其他断言和语句,如果必要的。
请注意,死亡测试只关心三件事:
1.statement是否中止或退出进程?
2.(在ASSERT_EXIT和EXPECT_EXIT的情况下)执行退出状态满足谓词?或者(在ASSERT_DEATH和EXPECT_DEATH的情况下)
退出状态非零吗?和
3. stderr 输出是否与matcher匹配?
特别是,如果statement生成ASSERT_*或EXPECT_*失败,则它不会**导致死亡测试失败,因为 kumotest 断言不会中止
的过程。
死亡测试命名
我们强烈建议您遵循命名约定
测试套件(不是测试)DeathTest,当它包含死亡测试时,如
在上面的例子中演示了。这
下面的死亡测试和线程 部分解释了原因。
如果正常测试和死亡测试共享一个测试夹具类,则可以使用
using 或 typedef 为夹具类引入别名并避免
复制其代码:
class FooTest : public testing::Test { ... };
using FooDeathTest = FooTest;
TEST_F(FooTest, DoesThis) {
// normal test
}
TEST_F(FooDeathTest, DoesThat) {
// death test
}
正则表达式语法
对于 POSIX 系统(Linux、Cygwin、Mac),kumotest 使用 POSIX扩展正则表达式 句法。要了解 POSIX 语法,您可能需要阅读此内容 维基百科条目。
在 Windows 上,kumotest 使用自己的简单正则表达式实现。 它缺乏许多功能。例如,我们不支持 union ("x|y")、分组
("(xy)")、括号 ("[xy]") 和重复次数 ("x{5,7}"),其中其他的。以下是我们支持的内容(“A”表示文字字符,句点
(.),或单个 \\ 转义序列; x 和 y 表示正则表达式。):
| 表达式 | 意义 |
|---|---|
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? | 匹配 0 或 1 次出现的A |
A* | 匹配 0 次或多次出现的A |
A+ | 匹配 1 次或多次出现的A |
^ | 匹配字符串的开头(不是每行的开头) |
$ | 匹配字符串的结尾(不是每一行的结尾) |
xy | 匹配x后跟y |
为了帮助您确定系统上可用的功能,kumotest
定义宏来控制它使用的正则表达式。宏是:
KTEST_USES_SIMPLE_RE=1 或 KTEST_USES_POSIX_RE=1。如果你想要你的死
测试在所有情况下都有效,您可以在这些宏上使用“#if”或使用 more
仅有限语法。
它是如何运作的
请参阅断言中的死亡断言参考。
死亡测试和线程
两种死亡测试风格的原因与线程安全有关。由于在线程存在的情况下分叉的众所周知的问题,死亡测试应该 在单线程上下文中运行。然而,有时这是不可行的安排这样的环境。例如,静态初始化的模块 可能会在到达 main 之前启动线程。一旦创建了线程,清理它们可能很困难或不可能。
kumotest 具有三个旨在提高对线程问题的认识的功能。
- 死亡测试时,如果多个线程正在运行,则会发出警告遭遇。
- 名称以“DeathTest”结尾的测试套件在所有其他测试套件之前运行测试。
- 它使用
clone()而不是fork()在 Linux 上生成子进程(clone()在 Cygwin 和 Mac 上不可用),因为fork()更有可能 当父进程有多个线程时导致子进程挂起。
在死亡测试语句中创建线程是完全可以的;他们是在单独的进程中执行,不会影响父进程。
死亡测试风格
引入threadsafe死亡测试风格是为了帮助缓解在可能的多线程环境中进行测试的风险。交易量增加
测试执行时间(可能会显着增加)以提高线程安全性。
自动化测试框架不设置样式标志。您可以选择一个通过以编程方式设置标志来进行特定类型的死亡测试:
KTEST_FLAG_SET(death_test_style, "threadsafe")
您可以在main()中执行此操作来设置二进制文件中所有死亡测试的样式,或在个别测试中。回想一下,在运行每个测试之前会保存标志
之后恢复,因此您无需自己执行此操作。例如:
int main(int argc, char** argv) {
testing::InitKumoTest(&argc, argv);
KTEST_FLAG_SET(death_test_style, "fast");
return RUN_ALL_TESTS();
}
TEST(MyDeathTest, TestOne) {
KTEST_FLAG_SET(death_test_style, "threadsafe");
// This test is run in the "threadsafe" style:
ASSERT_DEATH(ThisShouldDie(), "");
}
TEST(MyDeathTest, TestTwo) {
// This test is run in the "fast" style:
ASSERT_DEATH(ThisShouldDie(), "");
}
注意事项
ASSERT_EXIT()的statement参数可以是任何有效的 C++ 语句。如果
它通过return语句或抛出一个离开当前函数
例外,死亡测试被认为失败。一些 kumotest 宏
可能从当前函数返回(例如ASSERT_TRUE()),所以一定要避免
他们在statement中。
由于statement在子进程中运行,任何内存中的副作用(例如
修改变量、释放内存等)会导致不被观察到
在父进程中。特别是,如果你在死亡测试中释放记忆,
您的程序将无法通过堆检查,因为父进程永远不会看到
记忆被回收。要解决这个问题,您可以
- 死亡测试中尽量不要释放内存;
- 在父进程中再次释放内存;或者
- 不要在程序中使用堆检查器。
由于实现细节,您无法放置多个死亡测试断言在同一条线上;否则,编译将失败并出现不明 显的错误 信息。
尽管"threadsafe"死亡方式提高了线程安全性测试发现,在存在以下情况的情况下,仍然可能出现死锁等线程问题
使用“pthread_atfork(3)”注册的处理程序。
在子例程中使用断言
如果你想在子程序中放入一系列测试断言来检查 对于复杂的条件,请考虑使用 自定义 KMock 匹配器。这可以让你 在发生故障时提供更具可读性的错误消息并避免所有 问题如下所述。
向断言添加跟踪
如果从多个地方调用测试子例程,当其中的断言失败,很难判断失败的是子例程的哪个调用
从。您可以使用额外的日志记录或自定义失败来缓解此问题消息,但这通常会让你的测试变得混乱。更好的解决方案是使用
SCOPED_TRACE 宏或 ScopedTrace 实用程序:
SCOPED_TRACE(message);
ScopedTrace trace("file_path", line_number, message);
其中message可以是任何可流式传输到std::ostream的内容。 范围跟踪
宏将导致当前文件名、行号和给定消息
添加到每个失败消息中。 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"); // This trace point will be included in
18: // every failure in this scope.
19: Sub1(1);
20: }
21: // Now it won't.
22: Sub1(9);
23: }
could result in messages like these:
path/to/foo_test.cc:11: Failure
Value of: Bar(n)
Expected: 1
Actual: 2
Kumo 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的一些提示:
- 有了合适的消息,通常使用
SCOPED_TRACE就足够了 子例程的开始,而不是在每个调用站点。 - 在循环内调用子例程时,使循环迭代器成为循环迭代器的一部分
SCOPED_TRACE中的消息,以便您可以知道哪个迭代失败 是从. - 有时跟踪点的行号足以识别
子例程的特殊调用。在这种情况下,您不必
为
SCOPED_TRACE选择一个唯一的消息。您可以简单地使用""。 - 当外部作用域中有一个时,您可以在内部作用域中使用
SCOPED_TRACE范围。在这种情况下,所有 活动跟踪点都将包含在故障中 消息,按照它们遇到的相反顺序。 - 跟踪转储在 Emacs 中是可点击的 - 在行号上点击
return并 您将被带到源文件中的该行!
传播致命故障
使用ASSERT_*和FAIL*时的一个常见陷阱是不理解当它们失败时,它们只会中止_当前函数_,而不是整个测试。为了
例如,以下测试将出现段错误:
void Subroutine() {
// Generates a fatal failure and aborts the current function.
ASSERT_EQ(1, 2);
// The following won't be executed.
...
}
TEST(FooTest, Bar) {
Subroutine(); // The intended behavior is for the fatal failure
// in Subroutine() to abort the entire test.
// The actual behavior: the function goes on after Subroutine() returns.
int* p = nullptr;
*p = 3; // Segfault!
}
为了缓解这个问题,kumotest 提供了三种不同的解决方案。你可以使用异常、(ASSERT|EXPECT)_NO_FATAL_FAILURE断言或
HasFatalFailure() 函数。它们在下面两个中描述小节。