gMock 简易教程

什么是 gMock?

当您编写原型或测试时,完全依赖真实对象通常是不可行或不明智的。一个 模拟对象 实现了与真实对象相同的接口(因此可以像真实对象一样使用),但允许您在运行时指定如何使用它以及它应该做什么(哪些方法将被调用?以什么顺序?多少次?使用什么参数?它们将返回什么?等等)。

很容易将 伪对象 这个术语与模拟对象混淆。实际上,伪对象和模拟对象在测试驱动开发 (TDD) 社区中意味着非常不同的东西。

如果所有这些对你来说都太抽象了,别担心 - 最重要的是要记住,模拟对象允许你检查自身和使用它的代码之间的交互。一旦你开始使用模拟对象,伪对象和模拟对象之间的区别就会变得更加清晰。

gMock 是一个用于创建和使用模拟类的库(有时我们也会称它为“框架”,使其听起来很酷)。它对 C++ 所做的事情与 jMock/EasyMock 对 Java 所做的事情(或多或少)。

使用 gMock 时,

  1. 首先,您使用一些简单的宏来描述您想要模拟的接口,它们将扩展到您的模拟类的实现;
  2. 接下来,您创建一些模拟对象,并使用直观的语法指定其期望和行为;
  3. 然后,您执行使用模拟对象的代码。gMock 会在任何违反期望的情况发生时立即捕获它们。

为什么要使用 gMock?

虽然模拟对象可以帮助您消除测试中不必要的依赖项,并使测试快速且可靠,但在 C++ 中手动使用模拟对象却很难

相比之下,Java 和 Python 程序员有一些很好的模拟框架(jMock、EasyMock 等),它们可以自动创建模拟对象。因此,模拟是一种经过验证的有效技术,并在这些社区中得到广泛采用。拥有正确的工具绝对会带来改变。

gMock 的构建是为了帮助 C++ 程序员。它受到 jMock 和 EasyMock 的启发,但在设计时考虑了 C++ 的具体情况。如果以下任何问题困扰着您,它将是您的朋友:

我们鼓励您使用 gMock 作为

开始使用

gMock 与 googletest 捆绑在一起。

模拟海龟的案例

让我们看一个例子。假设您正在开发一个图形程序,该程序依赖于类似于 LOGO 的 API 进行绘图。您将如何测试它是否做了正确的事情?好吧,您可以运行它并将屏幕与黄金屏幕快照进行比较,但让我们承认:像这样的测试运行起来既昂贵又脆弱(如果您只是升级到具有更好抗锯齿功能的闪亮的新显卡会怎么样?突然,您必须更新所有黄金图像。)。如果您的所有测试都像这样,那将太痛苦了。幸运的是,您了解了 依赖注入 并知道该怎么做:不要让您的应用程序直接与系统 API 对话,而是将 API 包装在一个接口中(例如 Turtle)并针对该接口进行编码。

class Turtle {
  ...
  virtual ~Turtle() {}
  virtual void PenUp() = 0;
  virtual void PenDown() = 0;
  virtual void Forward(int distance) = 0;
  virtual void Turn(int degrees) = 0;
  virtual void GoTo(int x, int y) = 0;
  virtual int GetX() const = 0;
  virtual int GetY() const = 0;
};

(请注意,Turtle 的析构函数必须是虚函数,对于您打算从中继承的所有类都是如此 - 否则,当您通过基类指针删除对象时,将不会调用派生类的析构函数,并且您将获得损坏的程序状态,例如内存泄漏。)

您可以使用 PenUp()PenDown() 控制海龟的移动是否会留下痕迹,并使用 Forward()Turn()GoTo() 控制其移动。最后,GetX()GetY() 告诉您海龟的当前位置。

您的程序通常会使用此接口的真实实现。在测试中,您可以改用模拟实现。这使您可以轻松地检查您的程序正在调用哪些绘图原语,使用什么参数以及以什么顺序调用。以这种方式编写的测试更加健壮(它们不会因为您的新机器以不同的方式进行抗锯齿而中断),更易于阅读和维护(测试的意图在代码中表达,而不是在某些二进制图像中),并且运行快得多

编写模拟类

如果您很幸运,您需要使用的模拟对象已经被一些好心人实现了。但是,如果您发现自己需要编写一个模拟类,请放松 - gMock 将这项任务变成了一个有趣的游戏!(好吧,几乎是这样。)

如何定义它

Turtle 接口为例,以下是您需要遵循的简单步骤:

在此过程之后,您应该得到类似以下内容:

#include <gmock/gmock.h>  // Brings in gMock.

class MockTurtle : public Turtle {
 public:
  ...
  MOCK_METHOD(void, PenUp, (), (override));
  MOCK_METHOD(void, PenDown, (), (override));
  MOCK_METHOD(void, Forward, (int distance), (override));
  MOCK_METHOD(void, Turn, (int degrees), (override));
  MOCK_METHOD(void, GoTo, (int x, int y), (override));
  MOCK_METHOD(int, GetX, (), (const, override));
  MOCK_METHOD(int, GetY, (), (const, override));
};

您不需要在其他地方定义这些模拟方法 - MOCK_METHOD 宏将为您生成定义。就这么简单!

在哪里放置它

当您定义一个模拟类时,您需要决定将其定义放在哪里。有些人将其放在 _test.cc 中。当被模拟的接口(例如 Foo)由同一个人或团队拥有时,这很好。否则,当 Foo 的所有者更改它时,您的测试可能会中断。(您不能真的期望 Foo 的维护者修复每个使用 Foo 的测试,对吗?)

通常,您不应该模拟您不拥有的类。如果您必须模拟其他人拥有的此类,请在 Foo 的 Bazel 包中定义模拟类(通常是同一个目录或 testing 子目录),并将其放在 .h 和具有 testonly=Truecc_library 中。然后每个人都可以从他们的测试中引用它们。如果 Foo 发生更改,则只有一份 MockFoo 需要更改,并且只需要修复依赖于已更改方法的测试。

另一种方法:你可以在 Foo 之上引入一个薄层 FooAdaptor,并针对这个新接口进行编码。由于你拥有 FooAdaptor,因此可以更轻松地吸收 Foo 中的更改。虽然最初的工作量会更大,但仔细选择适配器接口可以使你的代码更易于编写和更具可读性(从长远来看是一个净收益),因为你可以选择 FooAdaptor 以更好地适应你的特定领域,而不是 Foo

在测试中使用 Mock

一旦你有了 mock 类,使用它就很简单了。典型的工作流程是

  1. testing 命名空间导入 gMock 名称,以便你可以不加限定地使用它们(每个文件只需执行一次)。记住命名空间是个好主意。
  2. 创建一些 mock 对象。
  3. 在它们上面指定你的期望(方法将被调用多少次?使用什么参数?它应该做什么?等等)。
  4. 执行一些使用 mock 的代码;可以选择使用 googletest 断言检查结果。如果 mock 方法被调用的次数超出预期或使用了错误的参数,你将立即收到错误。
  5. 当 mock 被销毁时,gMock 将自动检查它上面的所有期望是否都已满足。

这是一个例子

#include "path/to/mock-turtle.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>

using ::testing::AtLeast;                         // #1

TEST(PainterTest, CanDrawSomething) {
  MockTurtle turtle;                              // #2
  EXPECT_CALL(turtle, PenDown())                  // #3
      .Times(AtLeast(1));

  Painter painter(&turtle);                       // #4

  EXPECT_TRUE(painter.DrawCircle(0, 0, 10));      // #5
}

你可能已经猜到了,这个测试检查是否至少调用了一次 PenDown()。如果 painter 对象没有调用此方法,你的测试将失败,并显示如下消息

path/to/my_test.cc:119: Failure
Actual function call count doesn't match this expectation:
Actually: never called;
Expected: called at least once.
Stack trace:
...

提示 1: 如果你从 Emacs 缓冲区运行测试,则可以在行号上点击 <Enter> 以跳转到失败的期望。

提示 2: 如果你的 mock 对象从未被删除,则最终验证将不会发生。因此,当你将 mock 分配在堆上时,最好在测试中打开堆检查器。如果你已经使用了 gtest_main 库,你将自动获得它。

期望排序

重要提示: gMock 要求在调用 mock 函数之前设置期望,否则行为是未定义的。不要在调用 EXPECT_CALL() 和调用 mock 函数之间交替进行,并且不要在将 mock 传递给 API 后对其设置任何期望。

这意味着应将 EXPECT_CALL() 视为期望将来会发生调用,而不是已经发生了调用。为什么 gMock 这样工作?嗯,预先指定期望允许 gMock 在违规发生时立即报告,此时上下文(堆栈跟踪等)仍然可用。这使得调试更加容易。

诚然,这个测试是人为设计的,并没有做太多事情。你可以很容易地在不使用 gMock 的情况下实现相同的效果。但是,正如我们将很快揭示的那样,gMock 允许你使用 mock 做更多的事情

设置期望

成功使用 mock 对象的关键是在其上设置正确的期望。如果你将期望设置得过于严格,你的测试将由于不相关的更改而失败。如果你将它们设置得过于宽松,则可能会漏掉错误。你希望做得恰到好处,以便你的测试可以准确地捕获你打算捕获的那种错误。gMock 提供了必要的手段让你“恰到好处”地做到这一点。

通用语法

在 gMock 中,我们使用 EXPECT_CALL() 宏来设置 mock 方法上的期望。通用语法是

EXPECT_CALL(mock_object, method(matchers))
    .Times(cardinality)
    .WillOnce(action)
    .WillRepeatedly(action);

该宏有两个参数:首先是 mock 对象,然后是方法及其参数。请注意,两者之间用逗号(,)分隔,而不是句点(.)。(为什么要使用逗号?答案是技术原因所必需的。)如果该方法未重载,则也可以在不带匹配器的情况下调用该宏

EXPECT_CALL(mock_object, non-overloaded-method)
    .Times(cardinality)
    .WillOnce(action)
    .WillRepeatedly(action);

此语法允许测试编写者指定“使用任何参数调用”,而无需明确指定参数的数量或类型。为了避免意外的歧义,此语法只能用于未重载的方法。

该宏的任何形式都可以后跟一些可选的子句,这些子句提供有关期望的更多信息。我们将在后面的章节中讨论每个子句的工作方式。

此语法旨在使期望读起来像英语。例如,你可能可以猜到

using ::testing::Return;
...
EXPECT_CALL(turtle, GetX())
    .Times(5)
    .WillOnce(Return(100))
    .WillOnce(Return(150))
    .WillRepeatedly(Return(200));

表示 turtle 对象的 GetX() 方法将被调用五次,第一次将返回 100,第二次将返回 150,然后每次都返回 200。有些人喜欢将这种语法风格称为特定于领域的语言 (DSL)。

注意: 为什么我们使用宏来执行此操作?嗯,它有两个目的:首先,它使期望易于识别(通过 grep 或人工读者),其次,它允许 gMock 在消息中包含失败期望的源文件位置,从而使调试更加容易。

Matchers:我们期望什么参数?

当 mock 函数采用参数时,我们可以指定我们期望的参数,例如

// Expects the turtle to move forward by 100 units.
EXPECT_CALL(turtle, Forward(100));

通常你不想过于具体。还记得关于测试过于严格的讨论吗?过度规范会导致脆弱的测试并掩盖测试的意图。因此,我们鼓励你仅指定必要的—不多也不少。如果你对参数的值不感兴趣,请将 _ 作为参数写入,这意味着“一切皆有可能”

using ::testing::_;
...
// Expects that the turtle jumps to somewhere on the x=50 line.
EXPECT_CALL(turtle, GoTo(50, _));

_ 是我们称为 matchers 的一个实例。matcher 就像一个谓词,可以测试参数是否是我们期望的。你可以在期望函数参数的任何位置在 EXPECT_CALL() 中使用 matcher。 _ 是一种表示“任何值”的便捷方式。

在上面的示例中,10050 也是 matchers;隐式地,它们与 Eq(100)Eq(50) 相同,它们指定参数必须等于(使用 operator==)matcher 参数。有许多 内置的 matchers 用于常见类型(以及 自定义 matchers);例如

using ::testing::Ge;
...
// Expects the turtle moves forward by at least 100.
EXPECT_CALL(turtle, Forward(Ge(100)));

如果你不关心任何参数,则与其为每个参数指定 _,不如省略参数列表

// Expects the turtle to move forward.
EXPECT_CALL(turtle, Forward);
// Expects the turtle to jump somewhere.
EXPECT_CALL(turtle, GoTo);

这适用于所有非重载方法;如果方法已重载,则你需要通过指定参数的数量,并且可能还需要指定参数的 类型来帮助 gMock 解析期望的重载。

Cardinalities:将被调用多少次?

我们可以在 EXPECT_CALL() 之后指定的第一个子句是 Times()。我们将其参数称为 cardinality,因为它告诉调用应发生多少次。它允许我们重复一个期望多次,而无需实际多次编写它。更重要的是,cardinality 可以是“模糊的”,就像 matcher 可以是模糊的一样。这允许用户准确地表达测试的意图。

一个有趣的特例是当我们说 Times(0) 时。你可能已经猜到了 - 这意味着根本不应使用给定的参数调用该函数,并且每当(错误地)调用该函数时,gMock 都会报告 googletest 失败。

我们之前已经看到 AtLeast(n) 作为模糊 cardinality 的一个例子。对于你可以使用的内置 cardinality 列表,请参见 此处

可以省略 Times() 子句。如果省略 Times(),gMock 将为你推断 cardinality。 这些规则很容易记住

快速测验: 如果一个函数预期被调用两次,但实际上被调用了四次,你认为会发生什么?

Actions:它应该做什么?

还记得 mock 对象实际上没有工作实现吗?我们作为用户必须告诉它在调用方法时该怎么做。这在 gMock 中很容易。

首先,如果 mock 函数的返回类型是内置类型或指针,则该函数具有默认操作void 函数将仅返回,bool 函数将返回 false,而其他函数将返回 0)。此外,在 C++ 11 及更高版本中,返回类型是默认可构造的(即具有默认构造函数)mock 函数具有返回默认构造值的默认操作。如果你什么都不说,将使用此行为。

第二,如果模拟函数没有默认行为,或者默认行为不适合你,你可以使用一系列 WillOnce() 子句,后跟一个可选的 WillRepeatedly(),来指定每次期望匹配时要采取的动作。例如:

using ::testing::Return;
...
EXPECT_CALL(turtle, GetX())
     .WillOnce(Return(100))
     .WillOnce(Return(200))
     .WillOnce(Return(300));

表示 turtle.GetX() 将被调用正好三次(gMock 从我们编写的 WillOnce() 子句的数量推断出这一点,因为我们没有明确编写 Times()),并且将分别返回 100、200 和 300。

using ::testing::Return;
...
EXPECT_CALL(turtle, GetY())
     .WillOnce(Return(100))
     .WillOnce(Return(200))
     .WillRepeatedly(Return(300));

表示 turtle.GetY() 将被调用至少两次(gMock 知道这一点,因为我们编写了两个 WillOnce() 子句和一个 WillRepeatedly(),而没有显式编写 Times()),前两次将分别返回 100 和 200,从第三次开始返回 300。

当然,如果你显式编写了 Times(),gMock 就不会尝试自己推断基数。如果你指定的数字大于 WillOnce() 子句的数量怎么办?好吧,在所有 WillOnce() 用完之后,gMock 将每次都执行该函数的默认操作(除非你有一个 WillRepeatedly())。

除了 Return() 之外,我们还可以在 WillOnce() 内部做什么?你可以使用 ReturnRef(variable) 返回一个引用,或者调用一个预定义的函数,更多内容请参考 其他

重要提示: EXPECT_CALL() 语句只评估动作子句一次,即使该动作可能会执行多次。因此,你必须小心副作用。以下代码可能无法达到你想要的效果

using ::testing::Return;
...
int n = 100;
EXPECT_CALL(turtle, GetX())
    .Times(4)
    .WillRepeatedly(Return(n++));

这个模拟函数不会连续返回 100、101、102、…,而是始终返回 100,因为 n++ 只会被评估一次。同样,Return(new Foo) 将在执行 EXPECT_CALL() 时创建一个新的 Foo 对象,并且每次都返回相同的指针。如果你希望每次都发生副作用,则需要定义一个自定义动作,我们将在 cook book 中进行讲解。

又到了小测验时间!你认为以下代码意味着什么?

using ::testing::Return;
...
EXPECT_CALL(turtle, GetY())
    .Times(4)
    .WillOnce(Return(100));

显然,turtle.GetY() 预计会被调用四次。但是,如果你认为它每次都会返回 100,那就再想想!请记住,每次调用该函数时,都会消耗一个 WillOnce() 子句,然后采取默认操作。因此,正确的答案是 turtle.GetY() 第一次会返回 100,但从第二次开始返回 0,因为返回 0 是 int 函数的默认操作。

使用多个期望

到目前为止,我们只展示了你只有一个期望的示例。更现实的情况是,你将指定对多个模拟方法的期望,这些方法可能来自多个模拟对象。

默认情况下,当调用一个模拟方法时,gMock 将按照定义的相反顺序搜索期望,并在找到与参数匹配的活动期望时停止(你可以将其视为“较新的规则会覆盖较旧的规则”)。如果匹配的期望无法再接受任何调用,你将收到一个违反上限的错误。这是一个例子

using ::testing::_;
...
EXPECT_CALL(turtle, Forward(_));  // #1
EXPECT_CALL(turtle, Forward(10))  // #2
    .Times(2);

如果连续三次调用 Forward(10),则第三次调用将是一个错误,因为最后一个匹配的期望 (#2) 已饱和。但是,如果将第三次 Forward(10) 调用替换为 Forward(20),那么就可以了,因为现在 #1 将是匹配的期望。

注意: 为什么 gMock 按照期望的相反顺序搜索匹配项?原因是这允许用户在模拟对象的构造函数或测试夹具的设置阶段中设置默认期望,然后通过在测试主体中编写更具体的期望来自定义模拟。因此,如果你对同一个方法有两个期望,你希望将具有更具体匹配器的期望放在后面,否则更具体的规则将被后面的更一般的规则所遮蔽。

提示: 通常的做法是从一个方法的捕获所有期望开始,并使用 Times(AnyNumber())(省略参数,或者为所有参数使用 _,如果重载)。这使得对该方法的任何调用都符合预期。对于根本没有提及的方法(这些方法是“不感兴趣的”)来说,这不是必需的,但对于具有某些期望的方法很有用,但其他调用是可以接受的。请参阅 理解不感兴趣的调用与意外调用

有序调用与无序调用

默认情况下,即使尚未满足先前的期望,期望也可以匹配调用。换句话说,调用不必按照指定的期望顺序发生。

有时,你可能希望所有预期的调用都按照严格的顺序发生。在 gMock 中这样说很容易

using ::testing::InSequence;
...
TEST(FooTest, DrawsLineSegment) {
  ...
  {
    InSequence seq;

    EXPECT_CALL(turtle, PenDown());
    EXPECT_CALL(turtle, Forward(100));
    EXPECT_CALL(turtle, PenUp());
  }
  Foo();
}

通过创建 InSequence 类型的对象,其范围内的所有期望都被放入一个序列中,并且必须按顺序发生。由于我们只是依靠此对象的构造函数和析构函数来完成实际工作,因此其名称实际上无关紧要。

在此示例中,我们测试 Foo() 按照编写的顺序调用三个预期的函数。如果调用是乱序的,将会出现错误。

(如果你只关心某些调用的相对顺序,而不关心所有调用的相对顺序怎么办?你可以指定任意偏序吗?答案是……是的!详细信息可以在这里找到。)

所有期望都是粘性的(除非另有说明)

现在让我们做一个快速测验,看看你已经对这个模拟的东西掌握了多少。你将如何测试乌龟被要求精确两次到达原点(你想忽略它收到的任何其他指令)?

在你提出你的答案后,看看我们的答案并比较笔记(先自己解决它 - 不要作弊!)

using ::testing::_;
using ::testing::AnyNumber;
...
EXPECT_CALL(turtle, GoTo(_, _))  // #1
     .Times(AnyNumber());
EXPECT_CALL(turtle, GoTo(0, 0))  // #2
     .Times(2);

假设 turtle.GoTo(0, 0) 被调用了三次。第三次,gMock 将看到参数与期望 #2 匹配(请记住,我们总是选择最后一个匹配的期望)。现在,由于我们说过应该只有两次这样的调用,gMock 将立即报告错误。这基本上就是我们在上面的 使用多个期望 部分中告诉你的内容。

此示例表明 gMock 中的期望默认是“粘性的”,因为即使我们已经达到了它们的调用上限,它们仍然保持活动状态。这是一个需要记住的重要规则,因为它会影响规范的含义,并且与许多其他模拟框架中的做法不同(我们为什么要这样做?因为我们认为我们的规则使常见情况更容易表达和理解。)。

简单吗?让我们看看你是否真正理解了它:以下代码说明了什么?

using ::testing::Return;
...
for (int i = n; i > 0; i--) {
  EXPECT_CALL(turtle, GetX())
      .WillOnce(Return(10*i));
}

如果你认为它说明 turtle.GetX() 将被调用 n 次,并将连续返回 10、20、30、…,请三思!问题在于,正如我们所说,期望是粘性的。因此,第二次调用 turtle.GetX() 时,最后一个(最新的)EXPECT_CALL() 语句将匹配,并将立即导致“违反上限”错误 - 这段代码不是很有用!

turtle.GetX() 将返回 10、20、30、…的一种正确方法是明确指出期望不是粘性的。换句话说,它们应该在饱和时退休

using ::testing::Return;
...
for (int i = n; i > 0; i--) {
  EXPECT_CALL(turtle, GetX())
      .WillOnce(Return(10*i))
      .RetiresOnSaturation();
}

而且,有一种更好的方法可以做到这一点:在这种情况下,我们期望调用以特定的顺序发生,并且我们排列动作以匹配该顺序。由于顺序在这里很重要,我们应该使用一个序列来明确说明

using ::testing::InSequence;
using ::testing::Return;
...
{
  InSequence s;

  for (int i = 1; i <= n; i++) {
    EXPECT_CALL(turtle, GetX())
        .WillOnce(Return(10*i))
        .RetiresOnSaturation();
  }
}

顺便说一句,期望可能不是粘性的另一种情况是,当它在一个序列中时 - 只要序列中出现在它之后的另一个期望已经被使用,它就会自动退休(并且永远不会被用来匹配任何调用)。

不感兴趣的调用

一个模拟对象可能有很多方法,并非所有方法都那么有趣。例如,在某些测试中,我们可能不在乎 GetX()GetY() 被调用的次数。

在 gMock 中,如果你对一个方法不感兴趣,就什么也别说。如果发生了对该方法的调用,你会在测试输出中看到一个警告,但它不会是一个失败。这被称为“唠叨”行为;要更改它,请参阅 The Nice, the Strict, and the Naggy