GoogleTest 之路3-Mocking Framework

宇文嘉勋
2023-12-01

当你写一个原型或者测试的时候,依赖整个object 是不可行和明智的。一个 mock object和 real object 有同样的接口(所以它可以像同一个使用),但是让你在运行时进行指定它应该如何被使用,它应当做什么(哪些方法应该被调用?以何种顺序?多少次?用什么参数?什么会被返回?)

注意:很容易弄混 fake objects 和 mock objects。实际上fakes 和 mocks意味着不同的事情在Test-Driven Development(TDD)社区:

     Fake object(伪对象) 有 工作实现,但是经常采取一些捷径(也许让操作不昂贵),这会使它们不适合生产。内存中的文件系统就是一个例子。

     Mocks 是有期望的预编程对象。由一些预期会被调用的 sepcification组成。

如果这对你来说太抽象的话,don't worry- 你需要记住最重要的就是mock 允许你使用它与你的代码进行交互。当你使用mocks的时候,你对于fakes和 mocks的区别就更清晰了,

Google C++ Mocking Framework (or Google Mock for short) 是一个用来创建 mock 类库(叫“框架”是因为这样听起来更cool),就像 java 的 jMock and EasyMock

使用 Google Mock 包含以下三个基本步骤:

1. 使用一些简单的macros 来描述 你想mock的接口,这将扩展你的mock 类。

2. 用很直观的语法来描述一些mock对象的期望和行为。

3. 练习使用mock 对象的 代码。任何 违反 expectation 的行为一出现就会被Google Mock 捕获。

Why Google Mock?

为什么使用 Google Mock

虽然Mock Object 可以帮助你移除测试中不必要的依赖,并使它们快速可靠,但是在C ++中手动使用mock是很难的:

      有些人不得不实现mocks。这个工作既乏味又容易出错。难怪有些人想要避免它。

      手动写的mock的质量是不可靠的,无法预测的。你可能看过一些真正抛光过的,但是你也许会看到一些被匆忙砍掉有各种零时限制的。

      你从一个mock 获得的知识不能使用到下一个mock上面。

相比之下,Java和Python程序员有一些精细的模拟框架,自动创建mock。因此,Mock是一种被证明是有效的技术,并在这些社区广泛采用的做法。拥有正确的工具绝对有所不同。

Google Mock旨在帮助C ++程序员。它的灵感来自jMock和EasyMock,但是设计时考虑了C ++的细节。它会帮助你的,如果你遇到以下问题:

         你被不怎么好的设计所困扰,早知道应该做更多的原型设计的,但一切都太迟了,但是用C++进行原型设计速度会很慢。

          您的测试很慢,因为它们依赖于太多的库或使用昂贵的资源(例如数据库)

           你的测试是脆弱的,因为他们使用的一些资源是不可靠的(例如网络)

           您想要测试代码如何处理失败(例如,文件校验和错误),但是不容易去制造这么一个失败。

           你想确保你的当前模块和其他模块的交互是正确的,但是观察交互是很不容易的;因此你诉诸于观察行动结束时的副作用,这是最尴尬的

           你想 mock out 你的 依赖,除了还mock还没有被实现;坦白的讲,你对那些手写的mock 不感冒

我们鼓励你像这样使用Google Mock:

         一个设计工具,它可以让你早日经常尝试你的接口设计。更多的迭代导致更好的设计!

          一个测试工具,切断所有测试的外部依赖,探测你的模块和其他模块的交互!

Getting Started

开始吧

使用Google Mock很容易! 在你的C ++源文件中,只要#include“gtest / gtest.h”和“gmock / gmock.h”,你已经准备好了。

 

A Case for Mock Turtles

让我们来看一个例子。假设你在开发一个图形程序依赖一个 LOGO-like的 API 来绘图。你该怎样测试它做了正确的事情呢?你可以运行它并且与一个golden screen snapshot进行比较,但是我承认:像这样测试是昂贵的并且很脆弱(如果你要更新到一个全新抗锯齿的图像该怎么办?你要更新你所有的golden images),如果你的所有测试都是这样的,这就很痛苦了。Fortunately,你学习到了Dependency Injection并且知道该作什么:不要让你的application 直接 调用 drawing API, 把API包在一个接口里(say, Turtle) and code to that interface:

 

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()控制turtle的运动是否留下轨迹,并通过 Forward(),Turn()和GoTo()控制其运动。最后,GetX()和GetY()告诉你当前位置的turtle。

你的程序通常正常使用这个接口的实际实现。在测试中,你可以使用 实现的Mock来替换。这让你很容易的检查你程序你调用的 drawing primitives。传了哪些参数,以什么样的顺序。以这种方式编写的测试更强大,更容易读取和维护(测试的意图表示在代码中,而不是在一些二进制图像中)运行得多,快得多。

 

Writing the Mock Class

 

如果你幸运,你需要使用的mock已经被一些好的人实现。但是,你发现自己在写一个模拟class,放松- Google Mock将这个任务变成一个有趣的游戏!

How to Define It

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

1. MockTurtle继承Turtle类  

2.使用Turtle的虚函数(虽然可以使用模板来模拟非虚方法 mock non-virtual methods using templates,但是它更多的涉及)。计算它有多少参数。

3. 在 public 区: section of the child class, write MOCK_METHODn(); (or MOCK_CONST_METHODn(); if you are mocking a const method), where n is the number of the arguments; if you counted wrong, shame on you, and a compiler error will tell you so.

4. 现在来到有趣的部分:你采取函数签名,剪切和粘贴函数名作为宏的第一个参数,留下的作为第二个参数(如果你好奇,这是类型的功能)

5. 重复,直到您要模拟的所有虚拟功能完成。

After the process, you should have something like:

#include "gmock/gmock.h"  // Brings in Google Mock.
class MockTurtle : public Turtle {
 public:
  ...
  MOCK_METHOD0(PenUp, void());
  MOCK_METHOD0(PenDown, void());
  MOCK_METHOD1(Forward, void(int distance));
  MOCK_METHOD1(Turn, void(int degrees));
  MOCK_METHOD2(GoTo, void(int x, int y));
  MOCK_CONST_METHOD0(GetX, int());
  MOCK_CONST_METHOD0(GetY, int());
};

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

一旦你掌握了它,你可以快速的写出 mock class,以至于你的 source control system 都不能处理你的check-in 了

Tips: 如果 这对你来说工作量太大了,你可以在 Google Mock 的 scripts/generator/目录下面找到gmock_gen.py 工具。

Command-line 工具需要python2.4 安装。你只要给它一个 定义了抽象类的C++文件,它就会给你打印 其mock class。由于C++语言的复杂性,这个脚本可能不总是工作正常,但确实很有用,read the user documentation.

Where to Put It

当你定义了 mock class,你得决定你把这些定义放到什么地方。有些人把它放在一个* _test.cc。当这些 mock对象是被一个人或者一个团队使用的时候,这样定义就很好。否则,当Foo的所有者改变它,你的测试可能会中断。 (你不能真正期望Foo的维护者修复使用Foo的每个测试,你能吗?)

所以,经验法则是:如果你需要模拟Foo并且它由其他人拥有,在Foo的包中定义模拟类(更好的是,在一个测试子包中,你可以清楚地分离生产代码和测试实用程序),并且把它放在mock_foo.h。然后每个人都可以从它们的测试引用mock_foo.h。如果Foo变化,只有一个MockFoo的副本要更改,只有依赖于更改的方法的测试需要修复。

另外一种方法:你可以在Foo的顶部引入一个 薄层FooAdaptor ,并将代码引入这一新的接口。因为你拥有FooAdaptor,你可以更容易的吸收Foo的变化。虽然这是最初的工作,仔细选择适配器接口可以使您的代码更容易编写和更加可读性,因为你可以选择FooAdaptor适合你的特定领域比Foo更好。

 

Using Mocks in Tests

一旦你有了Mock 类,使用它非常容易。典型的工作流程如下:

1. 从测试命名空间导入Google Mock名称,以便您可以使用它们(每个文件只需执行一次。请记住,命名空间是一个好主意,有利于您的健康。)

2. 创建一些 mock对象

3.指定你对它们的期望(一个方法被调用多少次?有什么参数?它应该做什么等等)。

4.练习一些使用mock的代码; 可以使用Google Test断言检查结果。如果一个mock方法被调用超过预期或错误的参数,你会立即得到一个错误。

5. 当模mock destructed,Google Mock将自动检查是否满足了对其的所有期望

例子:

#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

int main(int argc, char** argv) {
  // The following line must be executed to initialize Google Mock
  // (and Google Test) before running the tests.
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}

正如你可能已经猜到的,这个测试检查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.

提示1:如果从Emacs缓冲区运行测试,您可以在错误消息中显示的行号上按<Enter>,直接跳到失败的预期。

提示2:如果你的mock object 从来没有被删除,最终的验证不会发生。因此,当您在堆上分配mock时,在测试中使用堆泄漏检查器是个好主意。

重要提示:Google Mock 需要expectation 在mock 函数被调用之前就设置,否者 行为就是 未定义的(undefined)。尤其是,你不能交错 EXPECT_CALL()和调用函数

这意味着EXPECT_CALL()应该被读取为期望call将在未来发生,而不是call已经发生。为什么Google Mock会这样工作?

好的,事先指定期望允许Google Mock在上下文(堆栈跟踪等)仍然可用时立即报告违例。这使得调试更容易。

诚然,这个测试是设计的,没有做太多。不使用Google Mock,您也可以轻松实现相同的效果。然而,正如我们将很快揭示的,Google Mock允许你做更多的。

Using Google Mock with Any Testing Framework

如果您要使用除Google测试(例如CppUnit或CxxTest)之外的其他测试框架作为测试框架,只需将上一节中的main()函数更改为:

int main(int argc, char** argv) {
  // The following line causes Google Mock to throw an exception on failure,
  // which will be interpreted by your testing framework as a test failure.
  ::testing::GTEST_FLAG(throw_on_failure) = true;
  ::testing::InitGoogleMock(&argc, argv);
  ... whatever your testing framework requires ...
}

这种方法有一个catch:它有时使Google Mock从一个模拟对象的析构器中抛出异常。对于某些编译器,这有时会导致测试程序崩溃。 你仍然可以注意到测试失败了,但它不是一个优雅的失败。

更好的解决方案是使用Google Test的事件侦听器APIevent listener API 来正确地向测试框架报告测试失败。 您需要实现事件侦听器接口的OnTestPartResult()方法,但它应该是直接的。

如果这证明是太多的工作,我们建议您坚持使用Google测试,它与Google Mock无缝地工作(实际上,它在技术上是Google Mock的一部分)。 如果您有某个原因无法使用Google测试,请告诉我们。

Setting Expectations

成功使用Mock Object的关键是对它设置正确的期望。 如果你设置的期望太严格,你的测试将失败作为无关的更改的结果。 如果你把它们设置得太松,错误可以通过。 你想做的只是正确的,使你的测试可以捕获到你想要捕获的那种错误。 Google Mock为您提供了必要的方法“恰到好处”。

General Syntax

在 Google Mock 中我们在 mock mecthod 中使用 EXPECT_CALL() 宏去设置expectation。 一般的语法是:

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

宏有两个参数:首先是mock对象,然后是方法及其参数。 请注意,两者之间用逗号(,)分隔,而不是句点(.)。 (为什么要使用逗号?答案是,这是必要的技术原因。)

宏之后可以是一些可选的子句,提供有关期望的更多信息。 我们将在下面的章节中讨论每个子句是如何工作的。

此语法旨在使期望读取如英语。 例如,你可能猜到

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或由人类读者),其次它允许Google Mock在消息中包括失败的期望的源文件位置,使调试更容易。

 

Matchers: What Arguments Do We Expect?

当一个mock函数接受参数时,我们必须指定我们期望什么参数; 例如:

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

 有些时候你也许不想要太具体(记住,谈论测试太僵硬,超过规范导致脆弱的测试和模糊测试的意图,因此,我们鼓励你只指定必要的 -不多也不少 ),如果你只关心 Forward() 会被调用,但是对 具体的参数不感兴趣,写_ 作为 参数,这意味“什么都可以”:

using ::testing::_;
...
// Expects the turtle to move forward.
EXPECT_CALL(turtle, Forward(_));

_是我们称为匹配器的实例.匹配器就像一个谓词,可以测试一个参数是否是我们期望的.你可以在EXPECT_CALL()里面使用一个匹配器来替换某一个参数。内置匹配器的列表可以在CheatSheet中找到。 例如,这里是Ge(大于或等于)匹配器:

using ::testing::Ge;
...
EXPECT_CALL(turtle, Forward(Ge(100)));

这检查,turtle将被告知前进至少100单位。

Cardinalities: How Many Times Will It Be Called?

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

一个有趣的特殊情况是当我们说Times(0)。你可能已经猜到了 - 这意味着函数不应该使用给定的参数,而且Google Mock会在函数被(错误地)调用时报告一个Google测试失败。我们已经看到AtLeast(n)作为模糊基数的一个例子。有关您可以使用的内置基数列表,请参见CheatSheet

Times()子句可以省略。如果你省略Times(),Google Mock会推断出你的基数。规则很容易记住:

  • 如果WillOnce()和WillRepeatedly()都不在EXPECT_CALL()中,则推断的基数是Times(1)。
  • 如果有n个WillOnce(),但没有WillRepeatedly(),其中n> = 1,基数是Times(n)
  • 如果有n个WillOnce()和一个WillRepeatedly(),其中n> = 0,基数是Times(AtLeast(n))。

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

Actions: What Should It Do?

记住,一个模拟对象实际上没有工作实现? 我们作为用户必须告诉它当一个方法被调用时该做什么。 这在Google Mock中很容易。

首先,如果一个模拟函数的返回类型是内置类型或指针,该函数有一个默认动作(一个void函数将返回,一个bool函数将返回false,其他函数将返回0)。

此外,在C ++ 11及以上版本中,返回类型为默认可构造(即具有默认构造函数)的模拟函数具有返回默认构造值的默认动作。 如果你不说什么,这个行为将被使用。

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

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

这说明turtle.GetX()将被调用三次(Google Mock从我们写的WillOnce()子句中推断出了这一点,因为我们没有明确写入Times()),并且会返回100,200, 和300。

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

turtle.GetY()将被调用至少两次(Google Mock知道这一点,因为我们写了两个WillOnce()子句和一个WillRepeatedly(),没有明确的Times()),将第一次返回100,200 第二次,300从第三次开始。

当然,如果你明确写一个Times(),Google Mock不会试图推断cardinality(基数)本身。 如果您指定的数字大于WillOnce()子句,该怎么办? 好了,毕竟WillOnce()已用完,Google Mock每次都会为函数执行默认操作(除非你有WillRepeatedly()。)。

除了Return()之外,我们可以在WillOnce()中做什么? 您可以使用ReturnRef(variable)返回引用,或调用预定义函数等。

重要说明:EXPECT_CALL()语句只评估一次操作子句,即使操作可能执行多次。 因此,您必须小心副作用。 以下可能不会做你想要的:

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

不是连续返回100,101,102,...,这个mock函数将总是返回100,因为n ++只被计算一次。 类似地,当执行EXPECT_CALL()时,Return(new Foo)将创建一个新的Foo对象,并且每次都返回相同的指针。 如果你想要每次都发生副作用,你需要定义一个自定义动作,我们将在 CookBook中教授。

另一个测验! 你认为以下是什么意思?

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

显然turtle.GetY()被期望调用四次。但如果你认为它会每次返回100,三思而后行!请记住,每次调用函数时都将使用一个WillOnce()子句,然后执行默认操作。所以正确的答案是turtle.GetY()将第一次返回100,但从第二次返回0,因为返回0是int函数的默认操作

Using Multiple Expectations

到目前为止,我们只列出了你有一个期望的例子。更现实地,你要指定对多个模拟方法的期望,这可能来自多个模拟对象。

默认情况下,当调用模拟方法时,Google Mock将按照它们定义的相反顺序搜索期望值,并在找到与参数匹配的活动期望时停止(您可以将其视为“新规则覆盖旧的规则“)。如果匹配期望不能再接受任何调用,您将得到一个上限违反的失败。这里有一个例子:

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

如果Forward(10)在一行中被调用三次,第三次它将是一个错误,因为最后的匹配期望(#2)已经饱和。然而,如果第三个Forward(10)被Forward(20)替换,则它将是OK,因为现在#1将是匹配期望。

附注:Google Mock为什么要以与预期相反的顺序搜寻匹配?原因是,这允许用户在模拟对象的构造函数中设置默认期望,或测试夹具的设置阶段中设置默认期望,然后通过在测试体中写入更具体的期望来定制模拟。所以,如果你对同一个方法有两个期望,你想把一个具有更多的特定的匹配器放在另一个之后,或更具体的规则将被更为一般的规则所覆盖。

Ordered vs Unordered Calls

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

有时,您可能希望所有预期的调用以严格的顺序发生。在Google Mock中说这很容易

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

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

通过创建类型为InSequence的对象,其范围中的所有期望都被放入序列中,并且必须按顺序发生。因为我们只是依靠这个对象的构造函数和析构函数做实际的工作,它的名字真的无关紧要。

在这个例子中,我们测试Foo()按照书写的顺序调用三个期望函数。如果调用是无序的,它将是一个错误。

如果你关心一些呼叫的相对顺序,但不是所有的呼叫,你能指定一个任意的部分顺序吗?答案是...是的!如果你不耐烦,细节可以在CookBook中找到。)

All Expectations Are Sticky (Unless Said Otherwise)

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

现在,让我们做一个快速测验,看看你可以多好地使用这个模拟的东西。你会如何测试,turtle被要求去原点两次(你想忽略任何其他指令)?

在你提出了你的答案,看看我们的比较的笔记(自己先解决 - 不要欺骗!):

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

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

这个例子表明,Google Mock的期望在默认情况下是“粘性”,即使在我们达到其调用上界之后,它们仍然保持活动。 这是一个重要的规则要记住,因为它影响规范的意义,并且不同于它在许多其他Mock框架中做的(为什么我们这样做?因为我们认为我们的规则使常见的情况更容易表达和 理解。)。

简单? 让我们看看你是否真的理解它:下面的代码说什么?

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()语句将匹配,并将立即导致“上限超过(upper bound exceeded)”错误 - 这段代码不是很有用!

一个正确的说法是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();
  }
}

Uninteresting Calls

模拟对象可能有很多方法,并不是所有的都是那么有趣。例如,在一些测试中,我们可能不关心GetX()和GetY()被调用多少次。

在Google Mock中,如果你对一个方法不感兴趣,只是不要说什么。如果调用此方法,您将在测试输出中看到一个警告,但它不会失败。

What Now?

恭喜!您已经学会了足够的Google Mock开始使用它。现在,您可能想要加入googlemock讨论组,并且实际上使用Google Mock编写一些测试 - 这很有趣。嘿,它甚至可以上瘾 - 你已经被警告。

然后,如果你想增加你的Mock商,你应该移动到 CookBook。您可以了解Google Mock的许多高级功能,并提高您的享受和测试幸福的水平。

转载于:https://www.cnblogs.com/qifei-liu/p/10897074.html

 类似资料: