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