使用Mockery的PHPUnit测试无法拦截被Mock类的方法调用
问题:Mockery无法拦截继承的protected方法init?
我使用PHPUnit 10.5.2和Mockery 1.6.11编写测试用例,测试DocumentsController的mergedDocuments方法。该控制器继承自Ias\AdminController,mergedDocuments方法会调用父类的protected方法init。我通过Mockery对DocumentsController做部分Mock,设置init方法接收指定的mockAdminView并返回true,但实际运行时init方法未被拦截,仍执行原逻辑返回false,导致mergedDocuments提前返回$view。请问为何init方法无法被拦截?
测试代码
public function testMergedDocumentsWithSuccessfulInit() { $params = new \Buan\Request\Parameters([], [], [], []); $mockAdminView = m::mock(AdminView::class); // 考虑继承自Ias\AdminController,对DocumentsController做部分Mock $controller = m::mock('DocumentsController', [$params]) ->makePartial() ->shouldAllowMockingProtectedMethods(); // Mock继承自Ias\AdminController的init方法 $controller->shouldReceive('init') ->with($mockAdminView) ->andReturn(true) ->once(); // 确保init方法被Mock后返回true一次 echo "Mock setup complete, testing init call\n"; $initResult = $controller->init($mockAdminView); // 这里应该返回true,因为init已被Mock echo "init called, result: " . ($initResult ? "true" : "false") . "\n"; // 执行mergedDocuments方法并检查结果 $result = $controller->mergedDocuments(); }
被测试的mergedDocuments方法片段
public function mergedDocuments(): AdminView { // 初始化 $view = new AdminView(); $initResult = $this->init($view); if (!$initResult) { return $view; } // 后续业务逻辑 }
init方法定义
protected function init(AdminView $view, bool $bypassAuthCheck = false): bool
原因与解决方案
核心原因
- 参数不匹配:你在Mock中指定的参数是
$mockAdminView,但mergedDocuments方法内部调用init时,传入的是新建的AdminView实例,并非你Mock的对象。Mockery的方法匹配是严格的,参数不匹配时会跳过Mock逻辑,执行原方法。 - 继承方法的Mock特性:当父类的protected方法未被子类重写时,Mockery的部分Mock对继承方法的拦截需要更精准的匹配规则,否则会优先执行父类的原方法。
解决方案
方案1:调整参数匹配逻辑,兼容实际调用
将Mock的参数匹配改为接受任意AdminView类型的实例,而不是指定某个特定Mock对象:
$controller->shouldReceive('init') ->with(m::type(AdminView::class)) // 匹配所有AdminView类型的实例 ->andReturn(true) ->once();
方案2:改造方法,支持注入AdminView(推荐)
修改mergedDocuments方法,允许外部传入AdminView实例,让测试可以直接传入Mock对象:
// 修改后的被测试方法 public function mergedDocuments(AdminView $view = null): AdminView { $view = $view ?? new AdminView(); // 无传入时使用默认实例 $initResult = $this->init($view); if (!$initResult) { return $view; } // 后续业务逻辑 } // 测试代码中传入Mock的AdminView $result = $controller->mergedDocuments($mockAdminView);
方案3:明确指定Mock父类的init方法
如果子类没有重写init方法,可以直接指定父类的方法进行Mock:
$controller->shouldReceive('Ias\AdminController::init') ->with(m::type(AdminView::class)) ->andReturn(true) ->once();
内容的提问来源于stack exchange,提问作者Tom Arnautovic
相关产品推荐
相关产品推荐

