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

如何正确用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链式调用的每一步:

  1. 先MockClient的messages方法,返回一个Mock的MessageList对象;
  2. 然后让这个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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 21:47:16