2021-02-16  21 views 评论

Google Test Mock学习笔记(上)

 标签:  

什么是gmock

一个mock对象实现了与真实对象相同的接口,但是它让您在运行时指定如何使用它以及应该做什么(包括它应该调用什么方法,以什么顺序调用方法,调用多少次,用什么参数,返回什么值等)。

当你使用gmock的时候:首先你要用一些简单的宏描述你希望mock的接口,这些宏将拓展为mock类的实现;其次要创建一些mock对象,并且使用直观的语法指定其期望或者行为;最后编写使用mock对象的代码,gMock将会在出现违反预期的时候立即捕获。

学习第一个gmock测试

写mock类

假设原来有一个类为

那么如何用gmock写一个测试呢? 第一步是写一个Mock类,下面是要做的步骤:

  • 首先是从Turtle类里面派生一个MockTurtle类。
  • Take a virtual function of Turtle (while it's possible to mock non-virtual methods using templates, it's much more involved).【这一步我大概认为是最好只能mock Turtle的 virtual函数】
  • 第三部是在这个子类里面写MOCK_METHOD();
  • 然后就是在MOCK_METHOD的括号里面写参数,这个参数实际上就是将父类的成员函数进行分解为【返回值类型】、【函数名】、【参数列表】、【const/override】。

如下是上面这个类的Mock类:

mock类放在哪里

当我们写完了这个类,我们应该将其放在哪里呢?

一些人喜欢将其放置在xxx_test.cpp文件中。如果被mock的接口一直是一个人或者一个团队在维护这是ok的,但是如果这个接口的维护之改变了,这个测试就有可能失败了。

所以,一般规定统一个原则:如果你需要mock一个类(假定为Foo)并且它属于其他人拥有,那么将mock class定义在Foo的package(更好的来说,定义在一个测试的子package里面,这样更清楚),将其放置在.h文件和一个cc_library。然后每一个人都可以在他们的测试中参考他们。如果这个类改变了,那么仅仅有一个MockFoo的copy会改变,而且仅有哪些依赖于改变内容的测试需要改变。

另一种实现该目的的方法是,你可以在Foo的顶层引入一个thin layer名为FooAdaptor,并将Foo置于FooAdaptor之下。由于你拥有FooAdaptor,你可以更轻松的处理好Foo中的修改。虽然这种方法在最初的时候需要做耕作的工作,但是这一做可以使你的代码更易于编写和更具有可读性。

使用mock类进行测试

使用的方法很简单,如下:

  1. 从testing的命名空间引入gMock的名字;
  2. 创建一些Mock对象;
  3. 指定你的期望(调用多少次,有什么参数,应该做什么等)
  4. 编写使用这些mocks的代码,可以使用gtest断言检查结果,如果mock方法的调用出现错误或者不符合预期,你会立即收到报错;
  5. 当mock被销毁的时候,gmock会自动检查我们所有的期望是否得到满足。

实现代码如下:

提示:
1. 如果你从Emacs缓冲区运行测试,则可以在行号上按Enter键跳转到失败的期望值;
2.如果你的mock对象从未删除,则最后的验证不会发生。因此,在堆上分配mocks的时候,应该在测试中打开堆检查器。如果你已经使用gtest_main库,则会自动获取该信息。
3.一定要在mock函数被调用之前设置期望值,否则行为将不确定,并且一定不能将EXPECT_CALL()和调用交错使用。

EXPECT_CALL()该如何写

能够使用mock来进行测试的一个关键点是设置正确的期望值。如果你设置的期望值太过于严苛,你的测试将会在存在不相关改变时失败;如果你设置的期望值太过于轻松,可能会导致一些bug不能被发现。您想正确地做它,以便您的测试可以准确地捕获您打算捕获的错误类型。 gMock为您“做到正确”提供了必要的手段。 通常的语法如下:

宏定义里面包括两个参数,分别是mock object和要调用的方法(包括参数)。如果方法没有重载(overloaded),那么可以在没有匹配器的情况下调用该宏:

这种语法允许测试编写者指定“使用任何参数调用”,而无需明确指定参数的数量或类型。为避免意外的歧义,此语法只能用于未重载的方法。 我们注意到,在宏定义后面还跟着三个子句,下面我们来讨论下这些子句的语法。我们举个例子,如下:

我们可以明白这句话的意思,我们希望调用turtle中的GetX()函数,并且完成以下动作:①调用5次;②第一次返回值为100;③第二次返回值为150;④此后每一次返回值都为200。

Matchers:我们期望怎样的参数

当一个mock函数需要参数的时候,我们需要指定我们期望的参数,例如

一般来说,我们可能不希望参数过于具体,即我们可能只对多个输入参数中只关心一个或几个,而对其它的要求遍历或者不关心。这个时候如果我们对其它参数也指定了具体的参数,可能会导致一些存在的bug我们无法测试到。因此,如果我们对这部分的参数不感兴趣或者需要遍历,可以使用_来替代具体参数值。例如:

但事实上,上面的100和50也都是matchers,它们实际上可以视同为Eq(100)和Eq(50)。 下面是一个内置的matchers,例如

如果您不关心任何参数,而不是为每个参数指定_,则可以省略参数列表:

这适用于所有非重载方法; 如果方法被重载,则需要通过指定参数数量以及可能的参数类型来帮助gMock解决预期的重载。

Cardinalities:将被调用多少次?

cardinality除了是具体的数字,也可以是模糊的,这非常有助于我们进行测试。如何模糊的使用呢,有以下几种方式:

AnyNumber()The function can be called any number of times.
AtLeast(n)The call is expected at least n times.
AtMost(n)The call is expected at most n times.
Between(m, n)The call is expected between m and n (inclusive) times.
Exactly(n) or nThe call is expected exactly n times. In particular, the call should never happen when n is 0.

一个特殊情况是当使用.Times(0)的时候,就意味着根本不应该使用给定的参数调用该函数,,并且无论何时(错误地)调用该函数,gMock都将报告googletest失败。 .Times()可以被忽略,如果忽略之,gMock将为您推断基数。 规则很容易记住:

  • 如果不存在WillOnce()或者WillRepeatedly(),则是Times(1);
  • 如果有n个WillOnce()且没有WillRepeatedly(),且n>=1,则是Times(n);
  • 如果有n个WillOnce()且有一个WillRepeadtedly(),并且n>=0,则是Times(AtLeast(n))。

Actions:实际做了什么

还记得模拟对象实际上没有有效的实现吗? 作为用户,我们必须告诉它在调用方法时该怎么做。

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

其次,如果mock函数没有默认操作或者默认操作不适合,则可以使用一系列的WillOnce()子句及可选的WillRepeatedly()指定每次期望匹配时要执行的操作,例如

这样的话,GetX()将要执行3次,分别返回100、200、300。

注意:
即使该动作可以执行多次,EXPECT_CALL()语句也仅对动作子句进行一次评估。 因此,您必须注意副作用。 以下内容可能无法满足您的要求:
using ::testing::Return;
...
int n = 100;
EXPECT_CALL(turtle, GetX())
.Times(4)
.WillRepeatedly(Return(n++));
我们可能期望它返回100、101、102、103,但实际上每一次返回都是100。我们可以这样理解,虽然重复进行4次,但是每一次都是独立的,故每一次返回都是100。

Using Multiple Expectations

到目前为止,我们一直使用的是只有一个期望的示例。但是在现实中,我们可能对多个mock对象的多个mock方法指定期望。

默认情况下,当调用mock方法时,gmock将按照与定义相反的顺序搜索期望值,并在找到与参数匹配的有效期望值的时候停止。如果匹配的期望值不能再进行更多的调用,就会收到一个失败的警告。例如:

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

Ordered vs Unordered Calls

默认情况下,即使未满足先前的期望,期望也可以匹配调用。 换句话说,调用不必按期望的指定顺序进行。 有时,您可能希望所有预期的调用都按照严格的顺序进行。 在gMock中说这很容易,如下:

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

在此示例中,我们测试Foo()按编写顺序调用了三个预期函数。 如果调用混乱,将会报错。

参考文献

https://github.com/google/googletest/blob/master/docs/gmock_cheat_sheet.md#CardinalityList

https://github.com/google/googletest/blob/master/docs/gmock_for_dummies.md

https://github.com/google/googletest/blob/master/docs/gmock_cook_book.md

给我留言

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: