旧版 gMock 常见问题

当我调用模拟对象上的方法时,实际对象的方法反而被调用了。这是什么问题?

为了模拟一个方法,它必须是*虚函数*,除非您使用高性能依赖注入技术

我可以模拟可变参数函数吗?

您不能直接在 gMock 中模拟可变参数函数(即,采用省略号(...)参数的函数)。

问题在于,通常,模拟对象*无法*知道有多少参数传递给可变参数方法,以及参数的类型是什么。只有*基类的作者*知道协议,我们无法窥探他或她的想法。

因此,要模拟这样的函数,*用户*必须教模拟对象如何确定参数的数量及其类型。一种方法是提供该函数的重载版本。

省略号参数继承自 C,并不是真正的 C++ 特性。它们使用起来不安全,并且不适用于具有构造函数或析构函数的参数。因此,我们建议在 C++ 中尽可能避免使用它们。

当我在定义带有 const 参数的模拟方法时,MSVC 给了我警告 C4301 或 C4373。为什么?

如果您使用 Microsoft Visual C++ 2005 SP1 编译此代码

class Foo {
  ...
  virtual void Bar(const int i) = 0;
};

class MockFoo : public Foo {
  ...
  MOCK_METHOD(void, Bar, (const int i), (override));
};

您可能会收到以下警告

warning C4301: 'MockFoo::Bar': overriding virtual function only differs from 'Foo::Bar' by const/volatile qualifier

这是一个 MSVC 错误。例如,相同的代码使用 gcc 编译得很好。 如果您使用 Visual C++ 2008 SP1,您会收到警告

warning C4373: 'MockFoo::Bar': virtual function overrides 'Foo::Bar', previous versions of the compiler did not override when parameters only differed by const/volatile qualifiers

在 C++ 中,如果您使用 const 参数声明一个函数,则 const 修饰符将被忽略。 因此,上面的 Foo 基类等效于

class Foo {
  ...
  virtual void Bar(int i) = 0;  // int or const int?  Makes no difference.
};

事实上,你可以使用 int 参数声明 Bar(),并使用 const int 参数定义它。编译器仍然会将它们匹配起来。

由于在方法声明中使参数 const 毫无意义,我们建议在 FooMockFoo 中删除它。这应该可以解决 VC 的错误。

请注意,我们这里讨论的是*顶层* const 修饰符。如果函数参数是通过指针或引用传递的,则将被指向者或被引用者声明为 const 仍然是有意义的。例如,以下两个声明等效

void Bar(int* p);         // Neither p nor *p is const.
void Bar(const int* p);  // p is not const, but *p is.

我无法弄清楚为什么 gMock 认为我的期望没有得到满足。我该怎么办?

您可能希望使用 --gmock_verbose=info 运行您的测试。此标志让 gMock 打印它收到的每个模拟函数调用的跟踪。 通过研究跟踪,您将了解为什么您设置的期望没有得到满足。

如果您看到消息“模拟函数没有设置默认动作,并且其返回类型没有设置默认值。”,则尝试添加默认动作。 由于已知问题,没有默认动作的模拟对象的意外调用不会打印实际参数和预期参数之间的详细比较。

我的程序崩溃了,并且 ScopedMockLog 输出了大量消息。 这是 gMock 的 bug 吗?

gMock 和 ScopedMockLog 很可能在这里做的是正确的。

当测试崩溃时,失败信号处理程序将尝试记录大量信息(例如堆栈跟踪和地址映射)。 如果您有许多具有深度堆栈的线程,这些消息会变得更加复杂。 当 ScopedMockLog 拦截这些消息并发现它们与任何期望不匹配时,它会为每个消息打印一个错误。

您可以学会忽略这些错误,或者您可以重写您的期望以使您的测试更健壮,例如,通过添加类似的内容

using ::testing::AnyNumber;
using ::testing::Not;
...
  // Ignores any log not done by us.
  EXPECT_CALL(log, Log(_, Not(EndsWith("/my_file.cc")), _))
      .Times(AnyNumber());

如何断言一个函数永远不会被调用?

using ::testing::_;
...
  EXPECT_CALL(foo, Bar(_))
      .Times(0);

我有一个失败的测试,其中 gMock 两次告诉我一个特定的期望没有得到满足。这不是很冗余吗?

当 gMock 检测到失败时,它会打印相关信息(模拟函数参数、相关期望的状态等),以帮助用户调试。 如果检测到另一个失败,gMock 将执行相同的操作,包括打印相关期望的状态。

有时,期望的状态在两次失败之间没有改变,您会看到相同的状态描述两次。 然而,它们不是冗余的,因为它们指的是不同的时间点。 它们相同的事实有趣的信息。

当使用模拟对象时,我遇到了堆检查失败,但使用真实对象却没事。 可能有什么问题?

您正在模拟的类(希望是一个纯接口)是否具有虚析构函数?

每当您从基类派生时,请确保其析构函数是虚函数。 否则,会发生糟糕的事情。 考虑以下代码

class Base {
 public:
  // Not virtual, but should be.
  ~Base() { ... }
  ...
};

class Derived : public Base {
 public:
  ...
 private:
  std::string value_;
};

...
  Base* p = new Derived;
  ...
  delete p;  // Surprise! ~Base() will be called, but ~Derived() will not
                 // - value_ is leaked.

通过将 ~Base() 更改为 virtual,当执行 delete p 时,将正确调用 ~Derived(),并且堆检查器将很高兴。

“较新的期望覆盖较旧的期望”规则使编写期望很尴尬。 为什么 gMock 这样做?

当人们抱怨这一点时,通常他们指的是类似这样的代码

using ::testing::Return;
...
  // foo.Bar() should be called twice, return 1 the first time, and return
  // 2 the second time.  However, I have to write the expectations in the
  // reverse order.  This sucks big time!!!
  EXPECT_CALL(foo, Bar())
      .WillOnce(Return(2))
      .RetiresOnSaturation();
  EXPECT_CALL(foo, Bar())
      .WillOnce(Return(1))
      .RetiresOnSaturation();

问题在于,他们没有选择表达测试意图的最佳方式。

默认情况下,期望不必以任何特定顺序匹配。 如果您希望它们以某种顺序匹配,则需要明确。 这是 gMock(和 jMock)的基本理念:很容易意外地过度指定您的测试,我们希望使其更难做到这一点。

有两种更好的方法来编写测试规范。 您可以将期望按顺序排列

using ::testing::Return;
...
  // foo.Bar() should be called twice, return 1 the first time, and return
  // 2 the second time.  Using a sequence, we can write the expectations
  // in their natural order.
  {
    InSequence s;
    EXPECT_CALL(foo, Bar())
        .WillOnce(Return(1))
        .RetiresOnSaturation();
    EXPECT_CALL(foo, Bar())
        .WillOnce(Return(2))
        .RetiresOnSaturation();
  }

或者你可以将一系列动作放在同一个期望中

using ::testing::Return;
...
  // foo.Bar() should be called twice, return 1 the first time, and return
  // 2 the second time.
  EXPECT_CALL(foo, Bar())
      .WillOnce(Return(1))
      .WillOnce(Return(2))
      .RetiresOnSaturation();

回到最初的问题:为什么 gMock 从后向前搜索期望(和 ON_CALL)? 因为这允许用户尽早为常见情况设置模拟对象的行为(例如,在模拟对象的构造函数或测试 fixture 的设置阶段),并稍后使用更具体的规则对其进行自定义。 如果 gMock 从前向后搜索,则这种非常有用的模式将无法实现。

当调用没有 EXPECT_CALL 的函数时,gMock 会打印警告,即使我已经使用 ON_CALL 设置了它的行为。 在这种情况下不显示警告是否合理?

在整洁和安全之间进行选择时,我们倾向于后者。 所以答案是我们认为显示警告更好。

通常,人们在模拟对象的构造函数或 SetUp() 中编写 ON_CALL,因为默认行为很少因测试而异。 然后在测试主体中,他们设置期望,这通常因每个测试而异。 在测试的设置部分中有一个 ON_CALL 并不意味着这些调用是预期的。 如果没有 EXPECT_CALL 并且调用了该方法,则可能是一个错误。 如果我们悄悄地让调用通过而不通知用户,则可能会在不知不觉中出现错误。

但是,如果您确定这些调用没问题,您可以编写

using ::testing::_;
...
  EXPECT_CALL(foo, Bar(_))
      .WillRepeatedly(...);

代替

using ::testing::_;
...
  ON_CALL(foo, Bar(_))
      .WillByDefault(...);

这告诉 gMock 您确实期望这些调用,并且不应打印任何警告。

此外,您可以通过指定 --gmock_verbose=error 来控制详细程度。 其他值为 infowarning。 如果您发现在调试时输出过于嘈杂,只需选择一个不太详细的级别。

如何在动作中删除模拟函数的参数?

如果你的模拟函数接受一个指针参数,并且你想删除该参数,你可以使用 testing::DeleteArg() 删除第 N 个(从零开始索引)参数

using ::testing::_;
  ...
  MOCK_METHOD(void, Bar, (X* x, const Y& y));
  ...
  EXPECT_CALL(mock_foo_, Bar(_, _))
      .WillOnce(testing::DeleteArg<0>()));

如何对模拟函数的参数执行任意操作?

如果您发现自己需要执行 gMock 不直接支持的某些操作,请记住您可以使用 MakeAction()MakePolymorphicAction() 定义自己的操作,或者您可以编写一个桩函数并使用 Invoke() 调用它。

using ::testing::_;
using ::testing::Invoke;
  ...
  MOCK_METHOD(void, Bar, (X* p));
  ...
  EXPECT_CALL(mock_foo_, Bar(_))
      .WillOnce(Invoke(MyAction(...)));

我的代码调用了一个静态/全局函数。我可以模拟它吗?

您可以,但您需要进行一些更改。

一般来说,如果您发现自己需要模拟一个静态函数,则表明您的模块耦合过于紧密(并且灵活性较差、可重用性较差、可测试性较差等)。 您最好定义一个小接口,并通过该接口调用该函数,然后可以轻松地模拟该接口。 最初需要做一些工作,但通常很快就会得到回报。

这篇 Google Testing Blog 帖子 很好地说明了这一点。 看看吧。

我的模拟对象需要做复杂的事情。 指定动作很痛苦。 gMock 真烂!

我知道这不是一个问题,但无论如何你都会得到一个免费的答案。 :-)

使用 gMock,您可以轻松地在 C++ 中创建模拟。 人们可能会想在任何地方都使用它们。 有时它们效果很好,有时您可能会发现它们,嗯,使用起来很痛苦。 那么,在后一种情况下有什么问题呢?

当您在不使用模拟的情况下编写测试时,您会执行代码并断言它返回正确的值,或者系统处于预期的状态。 这有时被称为“基于状态的测试”。

模拟非常适合一些人所说的“基于交互的”测试:模拟对象不是在最后检查系统状态,而是验证它们是否以正确的方式被调用,并在出现错误时立即报告错误,从而让您掌握触发错误的精确上下文。 这通常比基于状态的测试更有效和经济。

如果您正在进行基于状态的测试,并且使用测试替身只是为了模拟真实对象,那么您最好使用 fake。 在这种情况下使用模拟会导致痛苦,因为它不是模拟执行复杂动作的强项。 如果您遇到这种情况并认为模拟很糟糕,那么您只是没有为您的问​​题使用正确的工具。 或者,您可能试图解决错误的问题。 :-)

我收到警告“遇到不相关的函数调用 - 采取默认动作..” 我应该惊慌吗?

绝对不要! 这只是一个仅供参考的信息。 :-)

这意味着你有一个模拟函数,但你没有对它设置任何期望(根据 gMock 的规则,这意味着你对调用此函数不感兴趣,因此它可以被调用任意次数),并且它被调用了。这没问题 - 你并没有说调用该函数是不允许的!

如果你实际上想禁止调用此函数,但忘记编写 EXPECT_CALL(foo, Bar()).Times(0) 怎么办? 虽然可以认为这是用户的错误,但 gMock 试图友好地打印一个提示。

因此,当你看到该消息并认为不应该有任何不相关的调用时,你应该调查发生了什么。 为了让你的生活更轻松,gMock 在遇到不相关的调用时会转储堆栈跟踪。 从中你可以弄清楚是哪个模拟函数,以及它是如何被调用的。

我想定义一个自定义 action。 我应该使用 Invoke() 还是实现 ActionInterface 接口?

两种方式都可以 - 你应该选择对你来说更方便的方式。

通常,如果你的 action 是针对特定函数类型的,使用 Invoke() 定义它应该更容易; 如果你的 action 可以用在不同类型的函数中(例如,如果你定义 Return(*value*)),MakePolymorphicAction() 最容易。 有时你希望精确控制 action 可以用在哪些类型的函数中,那么实现 ActionInterface 是可行的方法。 有关示例,请参阅 gmock-actions.hReturn() 的实现。

我在 WillOnce() 中使用 SetArgPointee(),但 gcc 抱怨“指定了冲突的返回类型”。 这是什么意思?

你收到此错误是因为 gMock 不知道在调用模拟方法时应该返回什么值。 SetArgPointee() 说明了副作用是什么,但没有说明应该返回什么值。 你需要使用 DoAll()SetArgPointee()Return() 链接起来,后者提供适合被模拟 API 的值。

有关更多详细信息和示例,请参见此 示例

我有一个巨大的模拟类,并且 Microsoft Visual C++ 在编译它时耗尽了内存。 我能做什么?

我们已经注意到,当使用 /clr 编译器标志时,Visual C++ 在编译模拟类时会使用 5~6 倍的内存。 我们建议在编译原生 C++ 模拟时避免使用 /clr