I am writing a test case to send a sms using twilio sdk in php in a laravel application. I created a mock of the Client class, and I expect that the client will receive the messages then create methods and return a response.
Initialy, I wrote the following test, which return the error Mockery\Exception\BadMethodCallException: Received Mockery_0_Twilio_Rest_Client::getAccountSid(), but no expectations were specified
From the execution stack of the test, I see that the methods seems to be called on the real twilio SDK, which is not what I think it should do.
1) Tests\Unit\Services\Sms\TwilioAdapterTest::given_a_valid_phone_number_when_sending_sms_then_it_should_return_a_message_id
Mockery\Exception\BadMethodCallException: Received Mockery_0_Twilio_Rest_Client::getAccountSid(), but no expectations were specified
/home/aduhaime/Documents/projets/arm_sms/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010.php:128
/home/aduhaime/Documents/projets/arm_sms/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010.php:276
/home/aduhaime/Documents/projets/arm_sms/vendor/twilio/sdk/src/Twilio/Rest/Client.php:606
/home/aduhaime/Documents/projets/arm_sms/vendor/twilio/sdk/src/Twilio/Base/BaseClient.php:231
/home/aduhaime/Documents/projets/arm_sms/app/Services/Sms/TwilioAdapter.php:13
/home/aduhaime/Documents/projets/arm_sms/tests/Unit/Services/Sms/TwilioAdapterTest.php:31
/home/aduhaime/Documents/projets/arm_sms/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:173
The implementation
<?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]);
}
}
Failing test
<?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);
}
}
After some time, I found a way to make the test pass:
Passing test
<?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']);
}
}
Is this the right way to make that kind of test?
I feel like I should not have to know what method are called internally by the sdk and write shouldReceive
for getAcountSid() and request().
Your problem is this line:
$mock->shouldReceive('messages->create')
Mockery has support for "mocking Demeter chains", but it will assume the chain is composed of method calls, so this sets up the mock to respond to this:
$this->client->messages()->create(...);
However, what you're trying to mock is this:
return $this->client->messages->create(...);
Note that ->messages
is a property access, not a method call.
This may be clearer if you spell that out with some intermediate lines:
$client = $this->client; // 1
$messages = $client->messages; // 2
$result = $messages->create(...); // 3
return $result;
__get
, no way to tell here.create
method on whatever line 2 found.So what we actually want is to set the messages
property on the mock client, to itself be an appropriate mock.
The tricky part is knowing what class or interface to mock. Digging into the source code of Twilio\Rest\Client
, it looks to me like this is the (virtual) property we want to mock:
/**
* ...
* @property \Twilio\Rest\Api\V2010\Account\MessageList $messages
* ...
*/
Which would look something like this:
// Set up a mock
$mockMessageList = Mockery::mock('\Twilio\Rest\Api\V2010\Account\MessageList');
// Assign it to the `messages` property of the mock client
// this should over-ride the virtual property that the real client would use
$twilioClient->messages = $mockMessageList;
// Now we can set up expectations directly on the 'messages' mock
$mockMessageList->shouldReceive('create')->andReturn(json_encode(['sid' => '1234567890']));
// And inject the client into the adapter we're testing
$adapter = new TwilioAdapter($twilioClient);