Prophecy 不直接模拟方法调用,而是通过 $prophet->checkPredictions() 回溯验证声明的预期是否满足:未调用、次数错误或参数不匹配均抛异常;shouldReceive() 仅声明预期,非拦截替换;willBeCalled() 已弃用,应改用 willReturn(null);需手动调用 checkPredictions() 才能触发验证。

Prophecy 不能直接验证“行为”本身,它只验证你声明的预期是否被满足——没调用、调用次数错、参数不匹配,都会在 $prophet->checkPredictions()(或 PHPUnit 运行结束时)抛出异常。
为什么 shouldReceive() 不是“模拟方法调用”
很多人误以为 shouldReceive('doSomething') 是在“拦截并替换方法”,其实它只是在声明:“等下测试代码里必须调用这个方法,且满足我后续加的条件”。Prophecy 不会提前注入代理对象,也不修改原始类逻辑——它靠的是测试执行完后回溯检查调用记录。
- 如果你没在测试中真正调用该方法,
checkPredictions()会报MethodNotCalledException - 如果调用了但参数不匹配(比如期望
['id' => 123],实际传了['id' => 456]),同样失败 -
shouldReceive()后不接willReturn()或willThrow()是完全合法的——你只关心“是否被调用”,不关心返回值
willBeCalled() 和 willReturn() 的本质区别
willBeCalled() 是空操作桩(no-op stub),仅用于标记“此调用可接受”,不改变返回值;而 willReturn() 明确控制返回结果。两者常被混用,但语义和副作用不同:
-
->shouldReceive('save')->with($user)->willBeCalled():允许调用,返回null(PHP 默认),适合只验证流程不依赖返回值的场景 -
->shouldReceive('save')->with($user)->willReturn(true):强制返回true,适合下游逻辑依赖返回布尔值做分支判断 - 若同时写了
willBeCalled()和willReturn(),后者优先,前者被忽略 - 注意:Prophecy 7+ 已弃用
willBeCalled(),推荐统一用willReturn(null)替代
验证调用顺序和多次调用的陷阱
Prophecy 默认不关心调用顺序,也不区分“第一次”和“第二次”调用——除非你显式用 ->shouldBeCalledTimes(2) 或链式限定:
立即学习“PHP免费学习笔记(深入)”;
- 要验证“先调用
start(),再调用commit()”,得靠测试逻辑组织,Prophecy 本身不提供shouldBeCalledBefore()这类断言 - 验证两次不同参数的调用,必须分开声明:
$repo->shouldReceive('find')->with(1)->willReturn($user1); $repo->shouldReceive('find')->with(2)->willReturn($user2);否则第二次调用会因未匹配任何预言而失败 - 如果只写一次
shouldReceive('find')->willReturn(...)->shouldBeCalledTimes(2),则两次都必须传相同参数,否则报错 - 过度使用
shouldBeCalledTimes()容易让测试脆弱——比如重构时加了个缓存层导致调用次数变少,测试就挂,但业务逻辑未必错
与 PHPUnit 集成时最容易漏掉的一步
Prophecy 对象不是自动注册到 PHPUnit 的预测检查机制里的——你必须手动触发 $prophet->checkPredictions(),或者确保每个 prophecy 对象都被 $this->prophesize(...)->reveal() 后交由 PHPUnit 管理。常见疏忽:
- 在
setUp()里创建 prophecy,但在测试方法里忘了reveal(),结果传进被测类的是个 Prophecy\Prophecy\ObjectProphecy 实例,运行时报Call to undefined method - 用了
reveal(),但没在tearDown()或测试末尾调用checkPredictions(),导致本该失败的调用验证静默通过 - 多个 prophecy 共享一个
$prophet实例时,checkPredictions()会一并检查全部——别在中间提前调用,否则后续预言失效
最稳妥的做法:每个测试方法末尾加 $this->prophet->checkPredictions();,或在 tearDown() 统一处理,但前提是所有 prophecy 都来自同一个 $this->prophet 实例。



















