成功地使用 Mock 对象的关键是在它上面设置合适的期望。如果你设置的期望太过严格,你的测试可能会因为无关的改变而失败。如果你把期望设置的太过松驰, bugs可能会溜过去。而你需要的是你的测试可以刚好捕获你想要捕获的那一种 bug 。Google Mock 提供了一些方法可以让你的测试尺度 刚好 ( just right ) 。
在 Goolge Mock 中,我们用 EXPECT_CALL() 宏来设置一个 Mock 函数上的期望。一般语法是:
EXPECT_CALL(mock_object, method(matchers)) <xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" />
.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() 方法会被调用 5 次,它第一次返回 100 ,第二次返回 150 ,然后每次返回 200 。许多人喜欢称这种语法方式为特定领域语言 ( Domain-Specific Language (DSL) ) 。
注意: 为什么我们要用宏来实现呢?有两个原因:第一,它让期望更容易被认出来 (无论是 grep 还是人去阅读 ) ,第二,它允许 Google Mock 可以得到失败期望在源文件的位置,从而使 Debug 更容易。
当一个 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(_));
_ 是我们称为 Matchers 的一个例子,一个 matcher 是像一个断言,它可测试一个参数是否是我们期望的。你可用在 EXPECT_CALL() 中任何写函数参数期望的地方用matcher 。
一个内置的 matchers 可以在 CheatSheet 中找到,比如,下面是 Ge( greater than or equal ) matcher 的应用。
using ::testing::Ge;...
EXPECT_CALL(turtle, Forward(Ge(100)));
这个测试是检查 turtle 是否被告知要至少前进至少 100 个单位。
在 EXPECT_CALL() 之后第一个我们可以指定的子句是 Times() 。我们称 Times的参数为 cardinality ,因为它是指这个函数应该被调用 多少次 。 Times 可以让我们指定一个期望多次,而不用去写一次次地写这个期望。更重要的是, cardinality可以是“模糊”的,就像 matcher 一样。它可以让测试者更准确地表达他测试的目的。
一个有趣的特例是我们指定 Times(0) 。你也许已经猜到了,它是指函数在指定参数下不应该被调用,如果这个函数被调用了, Google Mock 会报告一个 Google Test 失败。
我们已经见过 AtLeast(n) 这个模糊 cardinalities 的例子了。你可以在CheatSheet 中找一个内置 cardinalities 列表。
Times() 子句可以省略。 如果你省略 Times() , Google Mock 会推断出cardinality 的值是什么。 这个规则很容易记:
l 如果在 EXPECT_CALL 中 既没有 WillOnce() 也没有 WillRepeatedly() ,那推断出的 cardinality 就是 Times(1) 。
l 如果有 n 个 WillOnce() ,但 没有 WillRepeatedl() ,其中 n >= 1 ,那么cardinality 就是 Times(n) 。
l 如果有 n 个 WillOnce() ,和一个 WillRepeatedly() ,其中 n >= 0 ,那么cardinality 就是 Times(AtLeast(n)) 。
小测试: 如果一个函数期望被调用 2 次,但被调用了 4 次,你认为会发生什么呢?
请记住一个 Mock 对象其实是没有实现的。是我们这些用户去告诉它当一个函数被调用时它应该做什么。这在 Google Mock 中是很简单的。
首先,如果 Mock 函数的返回类型是一个指针或是内置类型,那这个函数是有 默认行为 的 ( 一个 void 函数直接返回, bool 函数返回 false ,其它函数返回 0 ) 。如果你不想改变它,那这种行为就会被应用。
其次,如果一个 Mock 函数没有默认行为,或默认行为不适合你,你可以用WillOnce 来指定每一次的返回值是什么,最后可以选用 WillRepeatedly 来结束。比如:
using ::testing::Return;...
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(100))
.WillOnce(Return(200))
.WillOnce(Return(300));
上面的意思是 turtle.GetX() 会被调用 恰好 3 次,并分别返回 100 , 200 , 300 。
using ::testing::Return;...
EXPECT_CALL(turtle, GetY())
.WillOnce(Return(100))
.WillOnce(Return(200))
.WillRepeatedly(Return(300));
上面的意思是指 turtle.GetY() 将 至少 被调用 2 次,第一次返回 100 ,第二次返回200 ,从第三次以后都返回 300 。
当然,你如果你明确写上 Times() , Google Mock 不会去推断 cardinality 了。如果你指定的 cardinality 大于 WillOnce() 子句的个数时会发生什么呢?嗯,当WillOnce() 用完了之后, Google Mock 会每次对函数采用 默认 行为。
我们在 WillOnce() 里除了写 Return() 我们还能做些什么呢?你可以用ReturnRef( variable ) ,或是调用一个预先定义好的函数,自己在 Others 中找吧。
重要提示: EXPECT_CALL() 只对行为子句求一次值,尽管这个行为可能出现很多次。所以你必须小心这种副作用。下面的代码的结果可能与你想的不太一样。
int n = 100;
EXPECT_CALL(turtle, GetX())
.Times(4)
.WillRepeatedly(Return(n++));
它并不是依次返回 100 , 101 , 102... ,而是每次都返回 100 ,因为 n++ 只会被求一次值。类似的, Return(new Foo) 当 EXPECT_CALL() 求值时只会创建一个Foo 对象,所以它会每次都返回相同的指针。如果你希望每次都看到不同的结果,你需要定义一个自定义行为,我们将在 CookBook 中指导你。
现在又是一个小测验的时候了!你认为下面的代码是什么意思?
using ::testing::Return;...
EXPECT_CALL(turtle, GetY())
.Times(4)
.WillOnce(Return(100));
显然, turtle.Get() 期望被调用 4 次。但如果你认为它每次都会返回 100 ,那你就要再考虑一下了!记住,每次调用都会消耗一个 WillOnce() 子句,消耗完之后,就会使用默认行为。所以正确的答案是 turtle.GetY() 第一次返回 100 ,以后每次都返回 0 ,因为 0 是默认行为的返回值。
至今为止,我们只展示了如何使用单个期望。但是在现实中,你可能想指定来自不同 Mock 对象的 Mock 函数上的期望。
默认情况下,当一个 Mock 函数被调用时, Google Mock 会通过定义顺序的 逆序去查找期望,当找到一个与参数匹配的有效的期望时就停下来 ( 你可以把这个它想成是“老的规则覆盖新的规则“ ) 。如果匹配的期望不能再接受更多的调用时,你就会收到一个超出上界的失败,下面是一个例子:
using ::testing::_;...
EXPECT_CALL(turtle, Forward(_)); // #1
EXPECT_CALL(turtle, Forward(10)) // #2
.Times(2);
如果 Forward(10) 被连续调用 3 次,第 3 次调用它会报出一个错误,因为最后一个匹配期望 (#2) 已经饱和了。但是如果第 3 次的 Forward(10) 替换为 Forward(20) ,那它就不会报错,因数现在 #1 将会是匹配的期望了。
边注: 为什么 Google Mock 会以 逆序 去匹配期望呢?原因是为了可以让用户开始时使用 Mock 对象的默认行为,或是一些比较松驰的匹配条件,然后写一些更明确的期望。所以,如果你在同一个函数上有两个期望,你当然是想先匹配更明确的期望, 然后 再匹配其它的,或是可以说明确的规则会隐藏更宽泛的规则。
默认情况下,即使是在前一个期望没有被匹配的情况下,一个期望仍然可以被匹配。换句话说,调用的匹配顺序不会按照期望指定的顺序去匹配。
有时,你可能想让所有的期望调用都以一个严格的顺序来匹配,这在 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 的一个对象后,在这个对象作用域中的期望都会以 顺序 存放,并要求调用以这个 顺序 匹配。因为我们只是依赖这个对象的构造函数和析构函数来完成任务,所以对象的名字并不重要。
( 如果你只是关心某些调用的相对顺序,而不是所有调用的顺序?可以指定一个任意的相对顺序吗?答案是 ... 可以!如果你比较心急,你可以在 CookBook 中找到相关的细节。 )
现在让我们做一个小测验,看你掌握 Mock 到什么程度了。你如何测试 turtle 恰好 经过原点两次?
当你想出你的解法之后,看一下我们的答案比较一下 ( 先自己想,别作弊 ) 。
using ::testing::_;...
EXPECT_CALL(turtle, GoTo(_, _)) // #1
.Times(AnyNumber());
EXPECT_CALL(turtle, GoTo(0, 0)) // #2
.Times(2);
假设 turtle.GoTo(0,0) 被调用了 3 次。在第 3 次, Google Mock 会找到参数匹配期望 #2 。因为我们想要的是恰好经过原点两次,所以 Google Mock 会立即报告一个错误。上面的内容其实就是我们在“ Using Multiple Expectations ”中说过的。
上面的例子说明了 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, ... ,唉,你还是再想想吧!问题是,我们都说过了,期望是严格的。所以第 2 次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();
}
并且,有一个更好的解决方法,在这个例子中,我们期望调用以特定顺序执行。因为顺序是一个重要的因素,我们应该用 InSequence 明确地表达出顺序:
using ::testing::InSequence;
using ::testing::Return;
...
{
InSequence s;
for ( int i = 1; i <= n; i++) {
EXPECT_CALL(turtle, GetX())
.WillOnce(Return(10*i))
.RetiresOnSaturation();
}
}
顺便说一下,另一个期望可能 不 严格的情况是当它在一个顺序中,当这个期望饱和后,它就自动失效,从而让下一个期望有效。
一个 Mock 对象可能有很多函数,但并不是所有的函数你都关心。比如,在一些测试中,你可能不关心 GetX() 和 GetY() 被调用多少次。
在 Google Mock 中,你如果不关心一个函数,很简单,你什么也不写就可以了。如果这个函数的调用发生了,你会看到测试输出一个警告,但它不会是一个失败。
恭喜!你已经学习了足够的 Google Mock 的知识了,你可以开始使用它了。现在你也许想加入 googlemock 讨论组,并开始真正地用 Google Mock 开始写一些测试——它是很有意思的,嗨,这可能是会上瘾的,我可是警告过你了喔!
如果你想提高你的 Mock 等级,你可以移步至 CookBook 。你可以在那学习更多的Google Mock 高级特性——并提高你的幸福指数和测试快乐级别。