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

Laravel中Mock Twilio库测试消息发送遇401错误,如何解决?

问题:Laravel中Mock Twilio库测试WhatsApp消息发送失败(401认证错误)

我在Laravel项目里用Twilio的PHP库发送WhatsApp消息,现在想写测试用例Mock这个库,只验证create方法是否被正确调用,不需要发起真实请求。

控制器代码(SmsController.php)

$twilio = new Client('xxxxxxxxx', 'xxxxxxxxxxxxxxxxx');

$message = $twilio->messages->create(
    "whatsapp:+" . $request->input('user_phone'),
    [
        "mediaUrl" => [
            $request->input('url'),
        ],
        "from" => "whatsapp:+xxxxxxxxx",
    ]
);

return response(['message_sid' => $message->sid]);

当前测试代码

use Twilio\Rest\Client;
// ...

Http::fake();

$mock = $this->partialMock(Client::class, function (MockInterface $mock) {
    $mock->shouldReceive('create')
        ->with(
            "whatsapp:+xxxxxxxx",
            [
                "mediaUrl" => [
                    "https://url.com/image.jpg",
                ],
                "from" => "whatsapp:+xxxxxxxxx",
            ]
        );
});

$this->postJson('/twilio/user-message')
    ->assertOk();

测试错误信息

测试时抛出401认证错误:

[HTTP 401] Unable to create record: Authentication Error - invalid username

我猜测是构造函数需要凭证,但不想发起真实请求,该怎么解决?


解决方案

问题核心是你的Mock方式不对:你Mock了Client类的create方法,但实际代码中调用的是$twilio->messages->create——messages是Client的属性,返回的是MessageList对象,create是这个对象的方法,并非Client自身的方法。另外,控制器直接实例化Client会导致Mock无法拦截真实对象的创建,进而发起真实请求触发认证错误。

方案1:通过依赖注入重构控制器(推荐)

先把Twilio Client通过依赖注入注入控制器,这样测试时能轻松替换为Mock对象:

// 重构后的SmsController.php
use Twilio\Rest\Client;
use Illuminate\Http\Request;

class SmsController extends Controller
{
    protected $twilioClient;

    public function __construct(Client $twilioClient)
    {
        $this->twilioClient = $twilioClient;
    }

    public function sendUserMessage(Request $request)
    {
        $message = $this->twilioClient->messages->create(
            "whatsapp:+" . $request->input('user_phone'),
            [
                "mediaUrl" => [$request->input('url')],
                "from" => "whatsapp:+xxxxxxxxx",
            ]
        );

        return response(['message_sid' => $message->sid]);
    }
}

对应的测试代码:

use Twilio\Rest\Client;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
use Twilio\Rest\Api\V2010\Account\MessageList;

public function test_whatsapp_message_send()
{
    // Mock create方法返回的Message实例
    $messageInstanceMock = Mockery::mock(MessageInstance::class);
    $messageInstanceMock->shouldReceive('sid')->andReturn('SM_TEST_123');

    // Mock Client->messages返回的MessageList对象
    $messageListMock = Mockery::mock(MessageList::class);
    $messageListMock->shouldReceive('create')
        ->with(
            "whatsapp:+xxxxxxxx",
            [
                "mediaUrl" => ["https://url.com/image.jpg"],
                "from" => "whatsapp:+xxxxxxxxx",
            ]
        )
        ->andReturn($messageInstanceMock);

    // Mock Twilio Client,让它返回上面的MessageListMock
    $twilioClientMock = Mockery::mock(Client::class);
    $twilioClientMock->shouldReceive('messages')->andReturn($messageListMock);

    // 将Mock绑定到Laravel容器,控制器注入时会使用这个Mock
    $this->app->instance(Client::class, $twilioClientMock);

    // 发起测试请求,传入对应参数
    $this->postJson('/twilio/user-message', [
        'user_phone' => 'xxxxxxxx',
        'url' => 'https://url.com/image.jpg'
    ])
    ->assertOk()
    ->assertJson(['message_sid' => 'SM_TEST_123']);
}

方案2:不重构控制器,拦截Client实例化

如果不想修改控制器的代码,可以用Mockery的静态Mock拦截Client的实例化:

use Twilio\Rest\Client;
use Twilio\Rest\Api\V2010\Account\MessageInstance;
use Twilio\Rest\Api\V2010\Account\MessageList;

public function test_whatsapp_message_send_without_refactor()
{
    $messageInstanceMock = Mockery::mock(MessageInstance::class);
    $messageInstanceMock->shouldReceive('sid')->andReturn('SM_TEST_123');

    $messageListMock = Mockery::mock(MessageList::class);
    $messageListMock->shouldReceive('create')
        ->with(
            "whatsapp:+xxxxxxxx",
            [
                "mediaUrl" => ["https://url.com/image.jpg"],
                "from" => "whatsapp:+xxxxxxxxx",
            ]
        )
        ->andReturn($messageInstanceMock);

    // 拦截Client的实例化,忽略构造函数的凭证参数
    Mockery::mockStatic(Client::class, function ($mock) use ($messageListMock) {
        $mock->shouldReceive('__construct')
            ->withAnyArgs()
            ->andReturnSelf();
        $mock->shouldReceive('messages')->andReturn($messageListMock);
    });

    $this->postJson('/twilio/user-message', [
        'user_phone' => 'xxxxxxxx',
        'url' => 'https://url.com/image.jpg'
    ])
    ->assertOk()
    ->assertJson(['message_sid' => 'SM_TEST_123']);
}

关键注意事项

  • 确保测试时传入的user_phone和url参数与with()方法中的匹配,否则Mock断言会失败
  • 不需要在测试中传入真实的Twilio密钥,用withAnyArgs()忽略构造函数的凭证参数即可
  • 核心是MockMessageList的create方法,而非Client类的方法

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 11:14:50