GTEST做C++单元测试初级教程(GTEST Prime译文)

使用GTEST做C++单元测试

最近在学习GTEST,所以做了GTEST PRIME的翻译

[TOC]

好的测试应当遵循的守则,以及为什么要选择GTEST:

  • 测试应当是独立且可重现的。调试一个由于其他测试结果而导致成功或者失败的测试是一件痛苦的事情,GTEST在不同的对象上运行每个测试,从而将每个测试隔离开来,当一个测试失败时,GTEST可以单独调试那一个测试。
  • 测试应当是组织良好的并且能反应被测试代码的结构。
  • 测试应当是轻便且可重用的。谷歌有很多平台无关的代码,他们的测试也应当是平台无关的。GTEST可以运行在不同的系统上,和不同的编译器一起运行。
  • 当测试用例失败了,他们应该尽可能的提供多的信息。GTEST在第一测试失败时并不会停止,他只会停止当前的测试并继续运行下面的测试。这样便可以在一次变异运行周期内检测和修复多个bug。
  • GTEST将编写测试的开发人员从杂务中解放出来,使他们能够专心在测试内容上。
  • 编写测试应当便捷。GTEST可以重用共享的资源,只需要编写一次SetUpTearDown函数而不用每个测试都独立编写初始化和资源清理函数。

断言(Assertions)

GTEST的断言是类似函数调用的宏定义,对类或者函数使用断言来判断它的行为。当一个断言失败了,GTEST会打印这个测试断言的源文件、行数以及失败信息。也可以提供定制的错误信息接在GTEST的错误信息后面。

ASSERT_*EXPECT_*这两种断言成对出现,用来测试相同的东西,但是对当前函数有不同的影响。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;
}

基本的断言

判断真假的断言

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

比较断言

这类断言用来比较两个值

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

断言参数的值必须是可比较的,否则会产生一个编译错误。当断言失败时,如果自定义的错误支持<<运算符,那么GTEST将会打印他们,否则将会尝试用其他的方式打印出他们。

用户自定义类型仅仅当定义了比较操作时,断言才能够比较的对象的大小,但是这不被Google的C++类型规范所提倡,这种情况下应当使用ASSERT_TRUE()或者EXPECT_TRUE()来进行判断。不过还是应当尽可能的使用ASSERT_EQ(actual, expected),因为他能够在测试失败时告知actualexpected的值。

ASSERT_EQ()在比较指针时比较的是指针的值,当比较两个C风格的字符串时,将会比较他们是否有相同的内存地址,而不是有相同的值。因此在比较C风格字符串的时候应当使用ASSERT_STREQ(),但是在比较两个string对象的时候,应当使用ASSERT_EQ

在进行指针的比较时应当使用*_EQ(ptr, nullptr)*_NE(ptr, nullptr)代替*_EQ(ptr, NULL)*_NE(ptr, NULL),因为nullptr被定义了类型而NULL却没有。

当比较浮点数时应该使用浮点数断言来避免近似值导致的问题。

本节的宏定义对stringwstring都适用。

ps:2016年二月前的GTEST版本对*_EQ断言有着ASSERT_EQ(expected, actual)这样的顺序要求,但是新的*_EQ对两个参数顺序没有要求。

字符串比较

这节的断言用来比较C语言风格的字符串,在比较两个string对象时,应该使用EXPECT_EQ,EXPECT_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指针和空字符串不一样

简单的测试例子

创建一个测试:

  1. 使用TEST()宏定义来定义和命名一个测试函数,这些宏就是没有返回值的普通C++函数。
  2. 在这个函数中,可以包含任何有效的c++语句中,使用各种GTEST断言来检查值。
  3. 测试结果由断言决定;如果测试中的任何断言失败(致命或非致命),或者测试崩溃,则整个测试失败。
TEST(TestSuiteName, TestName) {
  ... test body ...
}

TEST()的第一个参数是测试套件(Test Suite)的名称,第二个参数是这个测试套件中该测试(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);
}

逻辑上来说,相关的测试应该在同一个测试套件(Test Suite)中。在上述的例子中,有两个测试HandlesZeroInputHandlesPositiveInput,他们属于同一个测试套件FactorialTest

Test Fixtures(为多个测试使用相同的配置)

当两个或更多的测试需要使用相似的数据时,可以使用Test Fixture。这可以对不同的测试重用相同的数据对象配置。

创建一个fixture:

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

当使用fixture时,使用TEST_F()代替TEST(),因为TEST_F()允许你在Test Fixture中获取对象和子程序:

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

TEST()类似,第一个参数是测试套件的名字,但是TEST_F()的这个参数必须和Test Fixture类的名字相同。还需要在使用Test Fixture对象之前定义这个Test Fixture类,否则会导致编译错误virtual outside class declaration

对于每个TEST_F()来说,GTEST在运行时都会创建一个新的test fixture对象,并且通过SetUp()立刻初始化这个对象,再运行测试,结束后通过调用TearDown()来进行清理工作,最后将删除这个test fixture对象。注意,在同一个测试套件中的不同测试拥有不同的test fixture对象,GTEST在新建下一个test fixture对象时总是会先删除上一个test fixture对象,并且不会在多个不同的测试中重用一个test fixture对象。所以如果任何测试改变了它的test fixture对象,并不会影响其他测试的test fixture对象。

下面用对一个FIFO队列类Queue编写测试,他有以下接口:

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(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(nullptr, n),因为我们稍后需要对指针n进行解引用,这将在n的值为NULL时导致段错误。

当测试运行时,以下步骤将会发生:

  1. GTEST构建一个QueueTest对象t1
  2. t1.SetUp()初始化t1
  3. 第一个测试在t1上运行
  4. t1.TearDown()在第一个测试结束时进行清理
  5. 析构t1
  6. 在进行另外一个QueueTest对象测试DequeueWorks测试时,重复上述步骤

调用测试

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

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

当调用RUN_ALL_TESTS()宏时:

  • 保存所有GTEST标志的状态
  • 为第一个测试创建一个test fixture对象
  • 通过SetUp()初始化这个对象
  • 在fixture对象上运行测试
  • 通过TearDown()函数进行清理
  • 删除fixture对象
  • 恢复所有GTEST标志的状态
  • 重复上述步骤直到测试结束

当一个致命性的错误发生时,后续的步骤将会被跳过。

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

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

编写main()函数

gtest_main库提供了一个合适的程序入口点,通过链接gtest_main动态库而不是gtest库,大多用户无需编写他们自己的main函数(Google Test提供了main()函数的基本实现。如果适合你的需求,则只需将测试与gtest_main库链接就可以了。)。本节的其余部分仅适用于需要在测试运行前做一些自定义的事情,而这些事情不能在test 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();
}

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

已知的限制

GTEST被设计成线程安全的。在使用pthread的系统上,GTEST的实现是线程安全的,而在其他系统(如Windows)上多线程并发使用Google Test的断言并不安全。一般情况下断言都是在主线程中进行的,因此在绝大多数测试中这并不会产生问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容