如何正确用Mockery测试Laravel中Twilio的SMS发送功能?
Laravel中Twilio SMS适配器的正确测试方案
问题背景
我在Laravel应用中使用Twilio PHP SDK编写发送SMS的测试用例,尝试MockTwilio\Rest\Client类,期望验证客户端调用messages->create方法并返回响应。但最初的测试报错:
Mockery\Exception\BadMethodCallException: Received Mockery_0_Twilio_Rest_Client::getAccountSid(), but no expectations were specified
从栈信息看,测试触发了SDK的真实内部方法,不符合预期。
实现代码
<?php namespace App\Services\Sms; use Twilio\Rest\Client; class TwilioAdapter implements SendsSms { public function __construct(private Client $client) { } public function send(string $from, string $to, string $message) { return $this->client->messages->create($to, ['from' => $from, 'body' => $message]); } }
最初的失败测试代码
<?php namespace Tests\Unit\Services\Sms; use App\Services\Sms\TwilioAdapter; use Mockery\MockInterface; use Tests\TestCase; use Twilio\Rest\Client; class TwilioAdapterTest extends TestCase { private function getTwilioClientMock($mockFunction) { return $this->mock(Client::class, $mockFunction); } /** @test */ public function given_a_valid_phone_number_when_sending_sms_then_it_should_return_a_message_id() { /** @var \Twilio\Rest\Client $twilioClient */ $twilioClient = $this->getTwilioClientMock(function (MockInterface $mock) { $mock->shouldReceive('messages->create')->andReturn(json_encode(['sid' => '1234567890'])); }); $adapter = new TwilioAdapter($twilioClient); $response = $adapter->send('+1234567890', '+554569999', 'Hello World'); $this->assertNotNull(json_decode($response)->sid); } }
后来我找到一种能让测试通过的写法,但需要模拟SDK内部的getAccountSid()和request()方法,这显然不合理——测试不该依赖对SDK内部实现的了解。
临时通过的测试代码
<?php namespace Tests\Unit\Services\Sms; use App\Services\Sms\TwilioAdapter; use Mockery\MockInterface; use Tests\TestCase; use Twilio\Rest\Client; use Twilio\Http\Response; class TwilioAdapterTest extends TestCase { private function getTwilioClientMock($mockFunction) { return $this->mock(Client::class, $mockFunction); } /** @test */ public function given_a_valid_phone_number_when_sending_sms_then_it_should_return_a_message_id() { /** @var \Twilio\Rest\Client $twilioClient */ $twilioClient = $this->getTwilioClientMock(function (MockInterface $mock) { $mock->shouldReceive('messages') ->shouldReceive('getAccountSid') ->andReturn(MessageList::class) ->shouldReceive('create') ->shouldReceive('request') ->andReturn(new Response(200, '{"sid": "SM1234567890"}')); }); $adapter = new TwilioAdapter($twilioClient); $response = $adapter->send('+1234567890', '+554569999', 'Hello World'); $this->assertNotNull($response->toArray()['sid']); } }
请问这种测试方式是否正确?有没有不需要关心SDK内部方法的正确写法?
错误原因分析
最初的测试写法错误在于链式调用的Mock方式不对:$mock->shouldReceive('messages->create')这种写法并不能正确Mock链式调用。当代码执行$client->messages->create()时,首先会调用Client的messages方法获取MessageList实例,然后再调用该实例的create方法。你的写法没有正确Mockmessages方法的返回值,导致Mockery无法拦截后续调用,进而触发了SDK的真实逻辑,才会出现getAccountSid()未被Mock的错误。
而临时通过的测试写法虽然能跑通,但完全违背了单元测试的原则——单元测试应该只关注被测代码的输入输出和依赖交互,不需要关心依赖(Twilio SDK)的内部实现细节。
正确的测试实现
正确的做法是分别Mock链式调用的每一步:
- 先Mock
Client的messages方法,返回一个Mock的MessageList对象; - 然后让这个Mock的
MessageList对象Mockcreate方法,返回符合预期的响应(可以模拟Twilio的MessageInstance实例,或者一个包含sid的对象)。
修改后的测试代码如下:
<?php namespace Tests\Unit\Services\Sms; use App\Services\Sms\TwilioAdapter; use Mockery; use Mockery\MockInterface; use Tests\TestCase; use Twilio\Rest\Client; use Twilio\Rest\Api\V2010\Account\MessageInstance; class TwilioAdapterTest extends TestCase { /** @test */ public function given_a_valid_phone_number_when_sending_sms_then_it_should_return_a_message_id() { // 1. Mock MessageInstance,模拟Twilio返回的消息对象 $messageInstance = Mockery::mock(MessageInstance::class); $messageInstance->shouldReceive('getSid')->andReturn('SM1234567890'); // 如果你的代码依赖toArray()方法,也可以Mock这个方法 $messageInstance->shouldReceive('toArray')->andReturn(['sid' => 'SM1234567890']); // 2. Mock MessageList,让它的create方法返回上面的Mock对象 $messageList = Mockery::mock(\Twilio\Rest\Api\V2010\Account\MessageList::class); $messageList->shouldReceive('create') ->with( '+554569999', // 第一个参数是接收号码 ['from' => '+1234567890', 'body' => 'Hello World'] // 第二个参数是配置数组 ) ->andReturn($messageInstance); // 3. Mock Client,让它的messages方法返回Mock的MessageList $twilioClient = $this->mock(Client::class, function (MockInterface $mock) use ($messageList) { $mock->shouldReceive('messages')->andReturn($messageList); }); // 执行测试 $adapter = new TwilioAdapter($twilioClient); $response = $adapter->send('+1234567890', '+554569999', 'Hello World'); // 断言结果 $this->assertNotNull($response->getSid()); $this->assertEquals('SM1234567890', $response->getSid()); } }
关键要点
- Mock链式调用的每一层:不要试图用
messages->create这种简写,而是分别Mock每个方法的返回值; - 只Mock必要的交互:只需要Mock被测代码实际用到的方法(
messages和create),不需要关心SDK内部的getAccountSid、request等方法; - 模拟真实返回值结构:尽量模拟Twilio SDK返回的真实对象(比如
MessageInstance),而不是返回JSON字符串,这样测试更贴近真实业务逻辑。
内容的提问来源于stack exchange,提问作者naghal
相关产品推荐
相关产品推荐

