Skip to main content
Version: nightly 🚧

ktest 入门

简介:为什么选择 ktest?

ktest 帮助您编写更好的 C++ 测试。

ktest是测试技术团队开发的测试框架牢记 Kumo 的具体要求和限制。无论您从事的是 Linux、Windows 或 Mac,如果您编写 C++ 代码,ktest 可以帮助您。还有它 支持任何类型的测试,而不仅仅是单元测试。

那么什么才是好的测试,ktest 如何融入其中呢?我们相信:

  1. 测试应该是“独立的”和“可重复的”。调试测试很痛苦 由于其他测试而成功或失败。 ktest 隔离 通过在不同的对象上运行每个测试来进行测试。当测试失败时, ktest 允许您单独运行它以进行快速调试。
  2. 测试应该“组织良好”并反映测试的结构 代码。 ktest 将相关测试分组到可以共享数据的测试套件中 和子程序。这种常见模式很容易识别并进行测试 易于维护。当人们转换时,这种一致性特别有帮助 项目并开始开发新的代码库。
  3. 测试应该是“可移植的”和“可重复使用的”。 Kumo 有很多代码 平台中立;它的测试也应该是平台中立的。测试 适用于不同的操作系统,具有不同的编译器,有或没有 例外,因此 ktest 测试可以使用各种配置。
  4. 当测试失败时,他们应该提供尽可能多的有关问题的信息 尽可能。 ktest 不会在第一次测试失败时停止。相反,它 仅停止当前测试并继续下一个。您还可以设置 报告非致命故障的测试,然后当前测试继续。 因此,您可以在一次运行-编辑-编译中检测并修复多个错误 循环。
  5. 测试框架应该将测试编写者从家务劳动中解放出来 并让他们专注于测试内容。 ktest自动保留 跟踪定义的所有测试,并且不需要用户枚举它们 为了运行它们。
  6. 测试应该“快”。使用ktest,您可以重用共享资源 跨测试,只需支付一次安装/拆卸费用,无需进行 测试相互依赖。

由于 ktest 基于流行的 xUnit 架构,因此您会感觉不错如果您以前使用过 JUnit 或 PyUnit,则可以在家里使用。如果没有,大约需要 10 分钟学习基础知识并开始使用。那么我们走吧!

注意术语

注意

不同的定义可能会引起一些混淆 术语 测试测试用例测试套件,因此请小心误解这些术语。

从历史上看,ktest 开始使用术语“测试用例”进行分组 相关测试,而当前出版物,包括国际软件 测试资格委员会 (ISTQB) 材料和 各种关于软件质量的教科书,使用这个术语 测试套件 为此。

相关术语 Test,在 ktest 中使用时,对应于术语 ISTQB 和其他人的测试用例

术语 Test 通常具有足够广泛的含义,包括 ISTQB 的定义 Test Case,所以这里不是什么大问题。但术语 Test CaseKumo测试中使用的Kumo测试具有矛盾的意义,因此令人困惑。

ktest 最近开始用_Test Suite_替换术语_Test Case_。 首选 API 是 TestSuite。旧的 TestCase API 正在缓慢更新 已弃用并重构。

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

Meaningktest TermISTQB Term
使用特定输入值练习特定程序路径并验证结果TEST()Test Case

基础概念

使用 ktest 时,首先编写断言,它们是语句检查条件是否为真。断言的结果可以是成功非致命故障,或致命故障。如果发生致命故障,则会中止当前功能;否则程序将正常继续。

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

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

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

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

断言

ktest 断言是类似于函数调用的宏。你测试一个类或通过对其行为做出断言来发挥作用。当断言失败时, ktest 打印断言的源文件和行号位置,以及并显示失败消息。您还可以提供自定义失败消息,该消息将 被附加到 ktest 的消息中。

这些断言成对出现,测试相同的事物但具有不同的效果关于当前函数。 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对象。如果是宽字符串 (Windows 上UNICODE模式下的wchar_t*TCHAR*std::wstring)是 流式传输到断言,打印时它将被转换为 UTF-8。

KumoTest 提供了一组断言来验证您的代码以各种方式。您可以检查布尔条件、比较值 基于关系运算符,验证字符串值、浮点值和更多。甚至还有一些断言可以让您验证更复杂的 通过提供自定义谓词来声明。有关断言的完整列表由 KumoTest 提供,请参阅断言参考

简单测试

创建测试:

  1. 使用TEST()宏定义并命名测试函数。这些都是不返回值的普通 C++ 函数。
  2. 在此函数中,以及您想要包含的任何有效 C++ 语句,使用各种 ktest 断言来检查值。
  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);
}

ktest 按测试套件对测试结果进行分组,因此逻辑上相关的测试应该在同一个测试套件中;换句话说,他们的第一个论点 TEST() 应该是相同的。在上面的例子中,我们有两个测试,HandlesZeroInputHandlesPositiveInput,属于同一测试 套件"阶乘测试"

当命名你的测试套件和测试时,你应该遵循相同的约定: 为了命名函数和类

可用性:Linux、Windows、Mac。

Test Fixture: 使用相同的数据配置进行多个测试

如果您发现自己正在编写两个或多个对相似数据进行操作的测试,那么您 可以使用*Test Fixture*。这允许您重复使用相同的配置 用于几个不同测试的对象。

要创建夹具:

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

使用夹具时,请使用TEST_F()而不是TEST(),因为它允许您 访问测试装置中的对象和子例程:

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

TEST()类似,第一个参数是测试套件名称,但对于TEST_F() 这必须是测试夹具类的名称。您可能已经猜到:_F用于固定装置。

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

此外,您必须先定义一个测试夹具类,然后才能在 TEST_F(),否则您将收到编译器错误virtual outside class declaration

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

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

首先,定义一个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_;
};

在这种情况下,不需要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. ktest 构造一个 QueueTest 对象(我们称之为 t1)。
  2. t1.SetUp() 初始化 t1
  3. 第一个测试 (IsEmptyInitially) 在 t1 上运行。
  4. t1.TearDown() 在测试完成后进行清理。
  5. t1 被破坏。
  6. 在另一个QueueTest对象上重复上述步骤,这次运行“DequeueWorks”测试。

可用性:Linux、Windows、Mac。

调用测试

TEST()TEST_F() 隐式地将它们的测试注册到 ktest。所以, 与许多其他 C++ 测试框架不同,您不必重新列出所有您定义的测试以便运行它们。

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

调用时,RUN_ALL_TESTS()宏:

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

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

  • 通过SetUp()初始化它。

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

  • 通过 TearDown() 清理固定装置。

  • 删除夹具。

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

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

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

warning

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

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

可用性:Linux、Windows、Mac。

编写 main() 函数

大多数用户应该不需要编写自己的main函数,而是链接使用ktest_main(与ktest相反),它定义了一个合适的条目 观点。有关详细信息,请参阅本节末尾。本节的其余部分仅当您需要在测试运行之前执行一些自定义操作时才适用 无法在装置和测试套件的框架内表达。

如果您编写自己的“main”函数,它应该返回以下值RUN_ALL_TESTS().

您可以从这个样板开始:

#include "this/package/foo.h"

#include "ktest/ktest.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::InitKumoTest(&argc, argv);
return RUN_ALL_TESTS();
}

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

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

但也许您认为编写所有这些main函数工作量太大?我们完全同意你的观点,这就是为什么Kumo测试提供了一个基本的 main() 的实现。如果它符合您的需求,那么只需将您的测试链接到ktest_main 库,你就可以开始了。

{: .callout .note}

warning

ParseGUnitFlags() 已被弃用,取而代之的是 InitKumoTest()

已知限制

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