You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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()方法用法不正确,改为闭包形式即可解决;
  • Mockresolve()获取的实例,核心是在Laravel容器中绑定接口的Mock实现;
  • 仅需修正方法调用错误这一处极小改动就能跑通测试,后续可逐步优化依赖注入方式,提升代码可测试性。

内容的提问来源于stack exchange,提问作者Sarah Messer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 14:54:55