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()忽略构造函数的凭证参数即可 - 核心是Mock
MessageList的create方法,而非Client类的方法
内容的提问来源于stack exchange,提问作者DeveloperX
相关产品推荐
相关产品推荐

