如何在PHPUnit中替换被Mock类的方法(Laravel环境)
我正尝试将部分集成测试转换为单元测试:原有测试代码部分可用,但已长期未运行,被测代码为遗留但基本可用的代码。原有测试对数据库和外部资源的假设在单元测试环境中不成立。由于当前测试覆盖率较低,我希望在添加单元测试验证预期逻辑时,尽量减少对现有代码的修改。我知晓被测代码可能未遵循最佳实践,但希望尽可能先实现可用的单元测试,再修改现有运行代码。
我希望使用Mock替换外部调用,但在PHPUnit中遇到了困难。我了解过Mockery、Prophecy等框架,但现有测试均基于PHPUnit,因此更倾向于继续使用它。目前找到的示例要么无法运行,要么未展示被测代码,要么存在未明确说明的假设,尤其存在以下问题:
- 被测代码大量使用内置的
resolve()函数实现依赖注入,但我未找到相关文档,尤其是在单元测试Mock场景下的文档; - 虽然找到多个替换方法返回值的示例,但未找到能覆盖方法操作逻辑的示例;
- 当前环境为PHP 8.1.16、Laravel Framework 8.83.27,本次问题不涉及数据库访问。
测试用例草稿
<?php namespace Tests\Unit; use App\Services\Implementations\SarahHandler; use App\Services\Implementations\SarahRequest; use Tests\TestCase; class SarahCodeToExamineMocksTest extends TestCase { public function testChainHandling() { $mockRequest = $this->getMockBuilder(SarahRequest::class) ->setMethods(['sendRequest']) ->getMock(); $mockRequest->expects($this->once()) // assert mock's only be called once ->method('sendRequest') ->will({ $mockRequest->response = 'Mock Request Response'; return $mockRequest; }); $url = 'https://stackoverflow.com'; // Original source code uses this resolve() built-in. $handler = resolve(SarahHandler::class); $result = $handler->handle($url); // Assert it completed using the Mock assertEquals($result, 'Mock Request Response'); } }
待Mock的类(核心部分)
<?php namespace App\Services\Implementations; use App\Services\Interfaces\SarahRequestInterface; class SarahRequest implements SarahRequestInterface { private $url; private $response; public function setUrl(string $url): SarahRequestInterface { $this->url = $url; return $this; } public function sendRequest(): SarahRequestInterface { echo 'Calling the internet...'; $this->response = 'Real external string'; return $this; } public function getResponse(): string { return $this->response; } }
被测顶层类
<?php namespace App\Services\Implementations; use Illuminate\Support\Facades\Log; use App\Services\Interfaces\SarahRequestInterface; class SarahHandler { public function handle(string $url) { return $this->doStuff($url); } private function doStuff(string $url): string { $request = resolve(SarahRequestInterface::class); $response = $request->getResponse($url); return $response; } }
对应的接口
<?php namespace App\Services\Interfaces; interface SarahRequestInterface { public function setUrl(string $url): SarahRequestInterface; public function sendRequest(): SarahRequestInterface; public function getResponse(): string; }
运行错误信息
PHP Fatal error: Uncaught ParseError: syntax error, unexpected token "{" in /app/tests/Unit/SarahCodeToExamineMocksTest.php:21 Stack trace: #0 /app/vendor/phpunit/phpunit/src/Util/FileLoader.php(49): PHPUnit\Util\FileLoader::load() #1 /app/vendor/phpunit/phpunit/src/Framework/TestSuite.php(397): PHPUnit\Util\FileLoader::checkAndLoad() #2 /app/vendor/phpunit/phpunit/src/Framework/TestSuite.php(536): PHPUnit\Framework\TestSuite->addTestFile() #3 /app/vendor/phpunit/phpunit/src/TextUI/TestSuiteMapper.php(67): PHPUnit\Framework\TestSuite->addTestFiles() #4 /app/vendor/phpunit/phpunit/src/TextUI/Command.php(391): PHPUnit\TextUI\TestSuiteMapper->map() #5 /app/vendor/phpunit/phpunit/src/TextUI/Command.php(112): PHPUnit\TextUI\Command->handleArguments() #6 /app/vendor/phpunit/phpunit/src/TextUI/Command.php(97): PHPUnit\TextUI\Command->run() #7 /app/vendor/phpunit/phpunit/phpunit(98): PHPUnit\TextUI\Command::main() #8 {main} Next PHPUnit\TextUI\RuntimeException: syntax error, unexpected token "{" in /app/vendor/phpunit/phpunit/src/TextUI/Command.php:99 Stack trace: #0 /app/vendor/phpunit/phpunit/phpunit(98): PHPUnit\TextUI\Command::main() #1 {main} thrown in /app/vendor/phpunit/phpunit/src/TextUI/Command.php on line 99
已尝试的参考资料
- 某Stack Overflow问答讨论了硬编码依赖的Mock,但示例中的实例化方式与
resolve()不同,且部分函数已过时; - 某博客文章较新,但未涉及
resolve(),按其操作后ORM似乎失效; - Laravel官方文档未覆盖替换方法逻辑(如
SarahRequest->response赋值)或resolve()函数的Mock场景。
1. 修复测试代码的语法错误
你的测试代码中will()方法的写法不符合PHPUnit规范,will()需要接收回调闭包,正确写法如下:
$mockRequest->expects($this->once()) ->method('sendRequest') ->will($this->returnCallback(function() use ($mockRequest) { return $mockRequest; }));
注意:直接给$mockRequest->response赋值会报错,因为response是私有属性,应该通过预设getResponse()的返回值来模拟结果。
2. 正确Mock resolve()获取的实例
由于被测代码通过resolve(SarahRequestInterface::class)获取实例,核心是在Laravel服务容器中绑定该接口的Mock实例,替换原有实现。修正后的完整测试用例:
<?php namespace Tests\Unit; use App\Services\Implementations\SarahHandler; use App\Services\Interfaces\SarahRequestInterface; use Tests\TestCase; class SarahCodeToExamineMocksTest extends TestCase { public function testChainHandling() { // 创建接口的Mock(而非具体实现类) $mockRequest = $this->createMock(SarahRequestInterface::class); // 预设getResponse的返回值 $mockRequest->method('getResponse') ->willReturn('Mock Request Response'); // 将Mock绑定到容器,覆盖接口的默认实现 $this->app->instance(SarahRequestInterface::class, $mockRequest); $url = 'https://stackoverflow.com'; $handler = resolve(SarahHandler::class); $result = $handler->handle($url); $this->assertEquals($result, 'Mock Request Response'); } }
另外需注意:原SarahHandler的doStuff方法中,getResponse($url)的调用不符合接口定义(接口中getResponse无参数),必须先修正这个代码错误,否则测试会触发参数不匹配的报错。
3. 重构建议(最小侵入式优先)
短期(快速跑通测试,改动极小)
修正SarahHandler的doStuff方法,使其符合接口规范:
private function doStuff(string $url): string { $request = resolve(SarahRequestInterface::class); $request->setUrl($url)->sendRequest(); return $request->getResponse(); }
同时可以在测试中添加调用验证,确保流程符合预期:
$mockRequest->expects($this->once()) ->method('setUrl') ->with($url); $mockRequest->expects($this->once()) ->method('sendRequest');
长期(优化代码可测试性,符合最佳实践)
将SarahRequestInterface通过构造函数注入SarahHandler,替代方法内的resolve()调用:
class SarahHandler { private $request; public function __construct(SarahRequestInterface $request) { $this->request = $request; } public function handle(string $url) { return $this->doStuff($url); } private function doStuff(string $url): string { $this->request->setUrl($url)->sendRequest(); return $this->request->getResponse(); } }
此时测试无需操作容器,直接将Mock传入构造函数即可,更符合单元测试的隔离原则:
$handler = new SarahHandler($mockRequest); $result = $handler->handle($url);
总结
- 语法错误是因为
will()方法用法不正确,改为闭包形式即可解决; - Mock
resolve()获取的实例,核心是在Laravel容器中绑定接口的Mock实现; - 仅需修正方法调用错误这一处极小改动就能跑通测试,后续可逐步优化依赖注入方式,提升代码可测试性。
内容的提问来源于stack exchange,提问作者Sarah Messer

