Lzh on GitHub
为了使我们的期望生效,我们 必须 调用 Mockery::close(),最好在像 tearDown_after 这样的回调方法中调用(取决于我们是否将 Mockery 与其他框架集成)。这个静态调用会清理当前测试使用的 Mockery 容器,并执行任何用于验证期望的任务。

一旦我们创建了一个模拟对象,我们通常希望开始定义它的具体行为(以及它应如何被调用)。这时,Mockery 的期望声明就派上用场了。

声明方法调用期望

要告诉我们的测试替身期望调用某个方法,我们使用 shouldReceive 方法:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method');

这是所有其他期望和约束附加的起点。

我们可以声明多个方法调用期望:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method_1', 'name_of_method_2');

所有这些都会继承任何链式期望或约束。

我们可以在声明方法调用期望的同时,指定它们的返回值:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive([
    'name_of_method_1' => 'return value 1',
    'name_of_method_2' => 'return value 2',
]);

也有一种简写方式设置方法调用期望及其返回值:

$mock = \Mockery::mock('MyClass', ['name_of_method_1' => 'return value 1', 'name_of_method_2' => 'return value 2']);

所有这些都可以附加额外的链式期望或约束。

我们可以声明测试替身 不应期望调用 给定方法名:

$mock = \Mockery::mock('MyClass');
$mock->shouldNotReceive('name_of_method');

此方法是 shouldReceive()->never() 的便捷写法。

声明方法参数期望

对于我们声明期望的方法,可以添加约束,使得期望只适用于使用指定参数调用的方法:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->with($arg1, $arg2, ...);
// 或者
$mock->shouldReceive('name_of_method')
    ->withArgs([$arg1, $arg2, ...]);

可以使用内置匹配器类提供更灵活的参数匹配(后文将讲)。例如,\Mockery::any() 匹配 with() 参数列表中该位置传入的任意值。Mockery 也支持 Hamcrest 库匹配器,例如 Hamcrestanything() 等同于 \Mockery::any()

注意:这意味着所有附加的期望仅适用于使用这些 精确参数 调用的方法:

$mock = \Mockery::mock('MyClass');

$mock->shouldReceive('foo')->with('Hello');

$mock->foo('Goodbye'); // 抛出 NoMatchingExpectationException

这允许根据提供给方法的参数设置不同的期望。

使用闭包进行参数匹配

我们可以使用闭包匹配所有传入参数,而不是为每个参数提供内置匹配器:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->withArgs(closure);

提供的闭包接收所有传递给预期方法调用的参数。通过这种方式,只有当传递的参数使闭包的求值为真时,此预期才适用于该方法调用。

$mock = \Mockery::mock('MyClass');

$mock->shouldReceive('foo')->withArgs(function ($arg) {
    if ($arg % 2 == 0) {
        return true;
    }
    return false;
});

$mock->foo(4); // matches the expectation
$mock->foo(3); // throws a NoMatchingExpectationException

部分参数匹配

我们可以提供部分参数,只要模拟方法调用中包含这些值即可:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->withSomeOfArgs(arg1, arg2, arg3, ...);

预期的参数顺序不重要。检查预期的值是否包含在内,但类型必须匹配。

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('foo')
    ->withSomeOfArgs(1, 2);

$mock->foo(1, 2, 3);  // matches the expectation
$mock->foo(3, 2, 1);  // matches the expectation (passed order doesn't matter)
$mock->foo('1', '2'); // throws a NoMatchingExpectationException (type should be matched)
$mock->foo(3);        // throws a NoMatchingExpectationException

任意或无参数

我们可以声明期望匹配任意参数调用:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->withAnyArgs();

默认情况下就是 withAnyArgs(),除非另行指定。

也可以声明期望只匹配零参数调用:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->withNoArgs();

声明期望返回值

  • andReturnNull()andReturn([null]) 表示返回 null
  • andReturnUsing(closure, ...) 可根据方法调用的参数计算返回值。
  • andReturnArg(index) 返回调用时指定索引的参数。
  • andReturnSelf() 用于流接口,返回模拟对象自身。

可以使用 andReturn() 指定模拟方法的返回值:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andReturn($value);

这为预期的函数调用设置了返回值。

可以为多个返回值设置预期。通过提供一系列返回值,我们告诉 Mockery 在每次对该方法进行后续调用时应返回什么值。

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andReturn($value1, $value2, ...)

第一次调用将返回 $value1,第二次调用将返回 $value2

如果我们调用的次数多于我们声明的返回值数量,Mockery 将为任何后续的方法调用返回最后一个值。

$mock = \Mockery::mock('MyClass');

$mock->shouldReceive('foo')->andReturn(1, 2, 3);

$mock->foo(); // int(1)
$mock->foo(); // int(2)
$mock->foo(); // int(3)
$mock->foo(); // int(3)

同样的,使用另一种语法也可以实现:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andReturnValues([$value1, $value2, ...])

它接受一个简单的数组,而不是参数列表。返回值的顺序由给定数组的数字索引决定,一旦先前的返回值被用尽,最后一个数组元素将在所有后续调用中被返回。

以下两个选项主要用于与测试读者交流:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andReturnNull();
// or
$mock->shouldReceive('name_of_method')
    ->andReturn([null]);

它们将模拟对象的方法调用标记为返回 null 或无值。

有时我们想根据传递给方法的参数来计算方法调用的返回结果。我们可以使用 andReturnUsing() 方法来实现这一点,该方法接受一个或多个闭包:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andReturnUsing(closure, ...);

闭包可以通过像 andReturn() 那样作为额外参数传递来排队。

有时,回显方法调用时的某个参数会很有用。在这种情况下,我们可以使用 andReturnArg() 方法;要返回的参数由它在参数列表中的索引指定:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andReturnArg(1);

这将返回方法调用时参数列表中的第二个参数(索引 #1)。

我们目前无法将 andReturnUsing()andReturnArg()andReturn() 混合使用。

如果我们在模拟流式接口,以下方法将会很有帮助:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andReturnSelf();

它将返回值设置为被模拟的类名。

抛出异常

我们可以让模拟对象的方法抛出异常:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andThrow(new Exception);

它在被调用时会抛出给定的 Exception 对象。

除了对象,我们还可以传入 Exception 类、消息和/或代码,以便在从模拟方法抛出 Exception 时使用:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andThrow('exception_name', 'message', 123456789);

设置公共属性

结合一个预期来使用,这样当一个匹配的方法被调用时,我们可以使用 andSet()set() 方法,将模拟对象的公共属性设置为一个指定的值。

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->andSet($property, $value);
// or
$mock->shouldReceive('name_of_method')
    ->set($property, $value);

当我们需要调用被模拟类的真实方法并返回其结果时,passthru() 方法会告诉期望绕过返回队列。

$mock->shouldReceive('name_of_method')
    ->passthru();

它允许对真实方法应用期望匹配和调用计数验证,同时仍使用预期的参数调用真实的类方法。

passthru() 的作用是:当 name_of_method 被调用时,不要返回预设的值,而是直接执行这个方法在原始对象中的真实逻辑。

声明调用次数期望

除了对方法调用的参数和返回值设置期望外,我们还可以设置方法应被调用多少次的期望。

当调用次数的期望没有满足时,会抛出 \Mockery\Expectation\InvalidCountException 异常。

在我们的测试结束时,绝对需要调用 \Mockery::close(),例如在 PHPUnit 的 tearDown() 方法中。否则 Mockery 将不会验证对我们的模拟对象进行的调用。

我们可以声明预期的方法可能被 调用零次或多次

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->zeroOrMoreTimes();
  • 精确次数:

这是所有方法的默认设置,除非另有设置。

要告诉 Mockery 期望某个方法被调用的确切次数,我们可以使用以下方法:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->times($n);

其中 $n 是该方法应被调用的次数。

  • 常用快捷方法:

一些最常见的用例有它们的简写方法。

要声明预期的方法必须只被调用一次:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')->once();  // 调用一次
$mock->shouldReceive('name_of_method')->twice(); // 调用两次
$mock->shouldReceive('name_of_method')->never(); // 永不调用

调用次数修饰器

调用次数的期望可以设置修饰符。

  • 最少调用次数:

如果我们要告诉 Mockery 一个方法应该被调用的最少次数,我们使用 atLeast()

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->atLeast()
    ->times(3);

atLeast()->times(3) 意味着(在匹配方法参数的情况下)该调用必须至少被调用三次,但绝不能少于三次。

  • 最多调用次数:

同样,我们可以使用 atMost() 告诉 Mockery 一个方法应该被调用的最大次数:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->atMost()
    ->times(3);

atMost()->times(3) 意味着该调用被调用的次数不能超过三次。如果该方法完全没有被调用,期望仍然会得到满足。

  • 范围调用次数:

我们也可以使用 between() 设置一个调用次数范围:

$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method')
    ->between($min, $max);

这实际上与使用 atLeast()->times($min)->atMost()->times($max) 完全相同,但作为简写形式提供。它后面可以跟一个没有参数的 times() 调用,以保持 API 的自然语言可读性。

多次调用不同期望

如果一个方法预期被多次调用,并且带有不同的参数和/或返回值,我们可以简单地重复设置期望。当然,如果我们期望对不同方法进行多次调用,这也同样适用。

$mock = \Mockery::mock('MyClass');
// Expectations for the 1st call
$mock->shouldReceive('name_of_method')
    ->once()
    ->with('arg1')
    ->andReturn($value1)

    // 2nd call to same method
    ->shouldReceive('name_of_method')
    ->once()
    ->with('arg2')
    ->andReturn($value2)

    // final call to another method
    ->shouldReceive('other_method')
    ->once()
    ->with('other')
    ->andReturn($value_other);

期望声明工具

  • ordered():声明方法调用顺序。
    声明此方法预期以相对于类似标记方法的特定顺序被调用。
  • ordered(group):将方法分组,组内可任意顺序,但与组外顺序有关。
    此处的顺序由设置模拟时实际使用此修饰符的顺序决定。
    声明该方法属于一个有序组(可以命名或编号)。组内的方法可以以任何顺序调用,但组外有序的调用与该组相关联:
  • globally():声明跨多个模拟对象的调用顺序。
    我们可以进行设置,让 method1group1 之前被调用,而 group1 又在 method2 之前被调用。
    当在 ordered()ordered(group) 之前调用时,它声明此顺序应用于所有模拟对象(而不仅仅是当前模拟对象):
  • byDefault():标记默认期望,可被后续非默认期望覆盖。
    这允许在多个模拟对象之间指定顺序期望。
    byDefault() 将一个期望标记为默认。除非创建了非默认期望,否则默认期望将被应用:
  • getMock():返回当前模拟对象,方便链式操作:
    这些后来的期望会立即取代之前定义的默认期望。这很有用,这样我们就可以在单元测试的 setup() 方法中设置默认模拟,然后在需要时在特定测试中调整它们。
    从期望链中返回当前模拟对象:

在某些情况下很有用,例如当我们希望将模拟设置保留为单个语句时:

$mock = \Mockery::mock('foo')->shouldReceive('foo')->andReturn(1)->getMock();