Mockery expectations API详解:构建复杂测试场景
你是否在编写单元测试时,遇到过需要验证某个方法被调用的次数、参数是否符合预期,或者需要模拟不同返回值的情况?Mockery的Expectations API正是为解决这些问题而生。本文将带你深入了解如何使用Expectations API构建复杂的测试场景,让你的单元测试更加精准和高效。
读完本文后,你将能够:
- 掌握Mockery Expectations API的核心方法
- 构建带有参数验证的测试场景
- 设置复杂的返回值和异常抛出规则
- 验证方法调用次数和顺序
- 解决常见的测试难题
Expectations API基础
Mockery的Expectations API允许你为模拟对象设置预期行为,包括方法调用、参数验证、返回值和调用次数等。核心入口是shouldReceive()方法,用于声明对某个方法的期望。
$mock = \Mockery::mock('MyClass');
$mock->shouldReceive('name_of_method');
这条简单的代码声明了我们期望MyClass的name_of_method方法会被调用。但通常我们需要更具体的期望,比如指定参数、返回值和调用次数。
核心API概览
| 方法 | 作用 |
|---|---|
shouldReceive() | 声明对方法的期望 |
shouldNotReceive() | 声明方法不应被调用 |
with() | 指定方法参数 expectations |
andReturn() | 设置返回值 |
once()/twice()/times(n) | 指定调用次数 |
andThrow() | 设置抛出异常 |
完整的API文档可参考官方文档。
方法调用与参数验证
基本参数匹配
最常用的参数验证方式是使用with()方法,它可以精确匹配方法调用时的参数:
$mock->shouldReceive('foo')
->with('Hello');
$mock->foo('Hello'); // 匹配预期
$mock->foo('Goodbye'); // 抛出NoMatchingExpectationException
灵活的参数匹配器
除了精确匹配,Mockery还提供了多种灵活的参数匹配器:
// 匹配任何参数
$mock->shouldReceive('foo')->withAnyArgs();
// 不接受任何参数
$mock->shouldReceive('foo')->withNoArgs();
// 部分参数匹配
$mock->shouldReceive('foo')->withSomeOfArgs(1, 2);
// 使用闭包自定义匹配逻辑
$mock->shouldReceive('foo')->withArgs(function ($arg) {
return $arg % 2 == 0; // 只匹配偶数
});
参数捕获
有时需要捕获方法调用时的参数以便后续验证,使用Mockery::capture():
$temp = null;
$mock->shouldReceive('foo')
->with(Mockery::capture($temp))
->once();
$mock->foo(42);
echo $temp; // 输出42
返回值与异常处理
基本返回值设置
使用andReturn()方法可以为预期的方法调用设置返回值:
$mock->shouldReceive('getUser')
->andReturn(['id' => 1, 'name' => 'John']);
序列返回值
如果方法会被多次调用,可通过andReturn()传递多个参数设置序列返回值:
$mock->shouldReceive('count')
->andReturn(1, 2, 3);
echo $mock->count(); // 1
echo $mock->count(); // 2
echo $mock->count(); // 3
echo $mock->count(); // 3 (后续调用都返回最后一个值)
也可以使用andReturnValues()传递数组:
$mock->shouldReceive('count')
->andReturnValues([1, 2, 3]);
动态返回值
使用andReturnUsing()可以通过闭包动态生成返回值,闭包会接收方法调用时的参数:
$mock->shouldReceive('sum')
->andReturnUsing(function ($a, $b) {
return $a + $b;
});
echo $mock->sum(2, 3); // 5
返回参数值
使用andReturnArg()可以直接返回指定位置的参数:
$mock->shouldReceive('echo')
->andReturnArg(0); // 返回第一个参数
echo $mock->echo('Hello'); // Hello
抛出异常
使用andThrow()方法可以模拟方法抛出异常:
$mock->shouldReceive('fetch')
->andThrow(new Exception('Network error'));
// 或者传递异常类名和参数
$mock->shouldReceive('fetch')
->andThrow('RuntimeException', 'Invalid parameter', 500);
调用次数验证
验证方法被调用的次数是单元测试中的常见需求,Mockery提供了多种便捷方法:
基本调用次数
$mock->shouldReceive('save')->once(); // 必须调用一次
$mock->shouldReceive('delete')->twice(); // 必须调用两次
$mock->shouldReceive('log')->times(3); // 必须调用三次
$mock->shouldReceive('error')->never(); // 不应该被调用
调用次数范围
// 至少调用2次
$mock->shouldReceive('refresh')->atLeast()->times(2);
// 最多调用5次
$mock->shouldReceive('retry')->atMost()->times(5);
// 调用2-5次之间
$mock->shouldReceive('process')->between(2, 5);
动态调整调用次数
// 允许0次或多次调用
$mock->shouldReceive('notify')->zeroOrMoreTimes();
调用顺序验证
在复杂场景中,可能需要验证方法调用的顺序。使用ordered()方法可以实现这一点:
$mock->shouldReceive('open')->ordered();
$mock->shouldReceive('read')->ordered();
$mock->shouldReceive('close')->ordered();
// 正确的调用顺序
$mock->open();
$mock->read();
$mock->close();
// 如果顺序错误,如先调用read()会抛出异常
分组顺序验证
可以为ordered()指定分组名称,同一组内的方法可以不按顺序调用:
$mock->shouldReceive('connect')->ordered('init');
$mock->shouldReceive('authenticate')->ordered('init');
$mock->shouldReceive('query')->ordered('db');
$mock->shouldReceive('fetch')->ordered('db');
// 'init'组内的方法可以互换顺序
$mock->authenticate();
$mock->connect();
// 'db'组内的方法可以互换顺序
$mock->fetch();
$mock->query();
实战案例:用户服务测试
让我们通过一个实际案例来综合运用Expectations API。假设我们有一个用户服务类,需要测试其transferFunds()方法,该方法涉及多个依赖调用。
// 创建依赖模拟
$userRepo = \Mockery::mock('UserRepository');
$transactionRepo = \Mockery::mock('TransactionRepository');
$notificationService = \Mockery::mock('NotificationService');
// 设置用户仓库预期
$userRepo->shouldReceive('find')
->with(1)
->once()
->andReturn((object)['id' => 1, 'balance' => 1000]);
$userRepo->shouldReceive('find')
->with(2)
->once()
->andReturn((object)['id' => 2, 'balance' => 500]);
$userRepo->shouldReceive('update')
->twice()
->with(Mockery::type('object'));
// 设置交易仓库预期
$transactionRepo->shouldReceive('create')
->once()
->with([
'from_user_id' => 1,
'to_user_id' => 2,
'amount' => 300
])
->andReturn((object)['id' => 123]);
// 设置通知服务预期
$notificationService->shouldReceive('send')
->twice()
->with(Mockery::type('object'), Mockery::pattern('/funds transferred/'));
// 注入依赖并执行测试
$service = new UserService($userRepo, $transactionRepo, $notificationService);
$result = $service->transferFunds(1, 2, 300);
// 验证结果
assert($result === true);
在这个案例中,我们:
- 模拟了三个依赖组件
- 为每个依赖设置了详细的调用预期
- 验证了方法调用次数、参数和顺序
- 测试了整个业务流程
常见问题与最佳实践
避免过度指定
虽然详细的预期可以使测试更精确,但过度指定会导致测试脆弱。只验证那些对测试结果真正重要的交互。
使用默认期望
对于重复出现的期望,可以使用byDefault()设置为默认期望,简化测试代码:
$mock->shouldReceive('log')
->withAnyArgs()
->once()
->byDefault();
清理模拟对象
务必在测试结束时调用Mockery::close(),确保所有期望都得到验证:
public function tearDown(): void
{
\Mockery::close();
parent::tearDown();
}
使用部分模拟
当需要测试类的部分方法,同时模拟其他方法时,可以使用部分模拟:
$mock = \Mockery::mock('MyClass[methodToMock]');
$mock->shouldReceive('methodToMock')->andReturn('mocked');
// 其他方法会调用真实实现
总结
Mockery的Expectations API提供了强大而灵活的工具,帮助你构建复杂的测试场景。通过本文介绍的方法,你可以精确控制模拟对象的行为,验证方法调用的各个方面,从而编写出更健壮、更可维护的单元测试。
关键要点:
- 使用
shouldReceive()开始设置期望 - 利用
with()系列方法进行参数验证 - 使用
andReturn()系列方法设置返回值 - 通过
once()/times()等方法验证调用次数 - 使用
ordered()控制调用顺序 - 合理组织测试代码,避免过度指定
掌握这些技巧后,你将能够应对各种复杂的测试场景,提高单元测试的质量和效率。
如果你觉得本文对你有帮助,请点赞收藏,并关注我们获取更多关于Mockery和单元测试的优质内容。下期我们将介绍如何使用Mockery模拟静态方法和私有方法,敬请期待!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



