如何在PHPUnit中模拟未通过构造/方法传入的PHP类?
在PHPUnit中Mock内部实例化的类(类似JavaScript模块Mock)
问题场景
我在使用PHPUnit编写单元测试时,遇到一个问题:需要测试的类内部直接实例化了另一个类(未通过构造函数或方法参数传入),比如下面的SmsSender类中直接new Sms(),想知道PHPUnit中有没有类似JavaScript里Mock模块的实现方式,能够全局Mock这个SomeLibrary\Resource\Sms类,存根其方法并断言调用参数是否正确。
待测试业务代码
<?php use SomeLibrary\Resource\Sms; final class SmsSender { public function __construct(private array $config) {} public function send(string $phone, string $content): void { $sms = new Sms(); $sms->sendToOne($phone, $content, $this->config['sender']); } }
初始测试代码
<?php declare(strict_types = 1); namespace Tests\Unit\App\Services\SmsSender; use App\Services\SmsSender; use Tests\Helpers\TestCase; class SmsSenderTest extends TestCase { private array $config; private SmsSender $service; public function setUp(): void { $this->config = [ 'apiKey' => 'mock-api-key', 'apiSecret' => 'mock-api-secret', 'sender' => 'my-company-name' ]; $this->service = new SmsSender($this->config); } public function testSend(): void { // how to mock the SomeLibrary\Resource\Sms ? $this->service->send('+3344334', 'dfddfdfdf'); } }
解决方案1:使用Mockery别名Mock(无需修改业务代码)
该方案借助Mockery的alias:前缀实现全局类Mock,让业务代码中new Sms()生成的是Mock实例,无需改动原有业务逻辑。
实现后的测试代码
<?php declare(strict_types = 1); namespace Tests\Unit\App\Services\SmsSender; use App\Services\SmsSender; use Mockery; use Mockery\MockInterface; use SomeLibrary\Resource\Sms; use Tests\Helpers\TestCase; class SmsSenderTest extends TestCase { private array $config; private SmsSender $service; private MockInterface $smsMock; public function setUp(): void { parent::setUp(); $this->config = [ 'apiKey' => 'mock-api-key', 'apiSecret' => 'mock-api-secret', 'sender' => 'my-company-name' ]; // 创建Sms类的别名Mock $this->smsMock = Mockery::mock('alias:' . Sms::class); $this->service = new SmsSender($this->config); } public function testSend(): void { $phone = '+3344334'; $content = 'dfddfdfdf'; $sender = $this->config['sender']; // 断言sendToOne方法会被正确参数调用一次 $this->smsMock->shouldReceive('sendToOne') ->once() ->with($phone, $content, $sender); $this->service->send($phone, $content); } public function tearDown(): void { Mockery::close(); parent::tearDown(); } }
贡献者:[标注第一个解决方案提供者名称]
解决方案2:重构为依赖注入(更符合SOLID原则)
该方案通过重构业务代码,将Sms类的实例化转为依赖注入,既保持原有代码兼容性,又让测试时可直接传入Mock实例,代码结构更灵活可测。
重构后的业务代码
<?php use SomeLibrary\Resource\Sms; final class SmsSender { public function __construct( private array $config, private ?Sms $sms = null ) { // 未传入Sms实例时自动初始化,兼容原有调用逻辑 $this->sms = $sms ?? new Sms(); } public function send(string $phone, string $content): void { $this->sms->sendToOne($phone, $content, $this->config['sender']); } }
实现后的测试代码
<?php declare(strict_types = 1); namespace Tests\Unit\App\Services\SmsSender; use App\Services\SmsSender; use PHPUnit\Framework\MockObject\MockObject; use SomeLibrary\Resource\Sms; use Tests\Helpers\TestCase; class SmsSenderTest extends TestCase { private array $config; private SmsSender $service; private MockObject $smsMock; public function setUp(): void { parent::setUp(); $this->config = [ 'apiKey' => 'mock-api-key', 'apiSecret' => 'mock-api-secret', 'sender' => 'my-company-name' ]; // 使用PHPUnit原生MockBuilder创建Sms的Mock实例 $this->smsMock = $this->getMockBuilder(Sms::class) ->disableOriginalConstructor() ->getMock(); // 传入Mock实例初始化SmsSender $this->service = new SmsSender($this->config, $this->smsMock); } public function testSend(): void { $phone = '+3344334'; $content = 'dfddfdfdf'; $sender = $this->config['sender']; // 断言sendToOne方法的调用参数和次数 $this->smsMock->expects($this->once()) ->method('sendToOne') ->with($phone, $content, $sender); $this->service->send($phone, $content); } }
贡献者:[标注第二个解决方案提供者名称]
内容的提问来源于stack exchange,提问作者Kristi Jorgji
相关产品推荐
相关产品推荐

