Adam McGurk
Adam McGurk

Reputation: 476

phpunit9 mocked method still being executed

I'm trying to mock a call to the Twilio Rest API for a test I'm writing. This is the code I've written to mock:

$message = $twilioTest->getMockedMessageInstance(['body' => 'This won\'t exist']);
$twilioStub = $this->getMockBuilder(Twilio::class)->getMock();
$twilioStub->expects($this->once())->method('retrieveLastTextFromBot')->willReturn($message);

And the contents of the retrieveLastTextFromBot method are:

$messages = $this->twilioClient->messages->read([
    'to' => TwilioDefinitions::getToNumber(),
    'from' => getenv('TWILIO_NUMBER'),
], 1);
if (count($messages) !== 1) {
     throw new NoExtantTextException('No previous message from ' . getenv('TWILIO_NUMBER') . ' to ' . TwilioDefinitions::getToNumber());
}
return $messages[0];

But obviously, I don't want the contents of the retrieveLastTextFromBot method to execute, that's why I'm mocking it. For some reasons though, that method is executing, and I know that because in my phpunit failure I'm getting this error:

1) CronControllerTest::testRemindMethodErrorHandling
Twilio\Exceptions\RestException: [HTTP 403] Unable to fetch page: Resource not accessible with Test Account Credentials

/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Page.php:58
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Page.php:34
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessagePage.php:23
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php:147
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php:96
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php:118
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/app/YmcaScheduler/Utility/Twilio.php:33
/Users/adammcgurk/Desktop/ymca-scheduler-brains/src/app/YmcaScheduler/Controller/CronController.php:29
/Users/adammcgurk/Desktop/ymca-scheduler-brains/tests/CronControllerTest.php:22

And line 33 in Twilio.php is that fourth line of the retrieveLastTextFromBot() method, so it's executing.

And maybe I just understand mocking incorrectly, but what I thought was going to happen was the method wouldn't be executed at all, instead, phpunit would just force data to be returned from it.

How can I mock a method in phpunit without it actually being executed?

Upvotes: 0

Views: 278

Answers (1)

Solonl
Solonl

Reputation: 2512

Working example

I start this answer with a working example. There are two things you can check as explained in this answer. Please update your question with extra information/ details when parts of the answer fit your situation.

Add this code to tests/TwilioTest.php:

<?php 

use PHPUnit\Framework\TestCase;
use Twilio\Rest\Client;

class Twilio
{
    private $twilioClient;

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

    public function retrieveLastTextFromBot()
    {
        $messages = $this->twilioClient->messages->read([
            'to' => TwilioDefinitions::getToNumber(),
            'from' => getenv('TWILIO_NUMBER'),
        ], 1);
        if (count($messages) !== 1) {
             throw new NoExtantTextException('No previous message from ' . getenv('TWILIO_NUMBER') . ' to ' . TwilioDefinitions::getToNumber());
        }
        return $messages[0];
    }
}

class Sut
{
    private $twilio;
    public function __construct(Twilio $twilio)
    {
        $this->twilio = $twilio;
    }

    public function method()
    {
        $this->twilio->retrieveLastTextFromBot();
    }
}

final class TwilioTest extends TestCase
{
    //I created a plain php object for the message just for the example 
    private function getMockedMessageInstance(array $message)
    {
        return (object) $message;
    }

    public function test_sut_method(): void
    {
$message = $this->getMockedMessageInstance(['body' => 'This won\'t exist']);
        $twilioStub = $this
            ->getMockBuilder(Twilio::class)
            ->disableOriginalConstructor() # I have added this to ignore the Twilio constructor dependencies
            ->getMock();
        $twilioStub->expects($this->once())->method('retrieveLastTextFromBot')->willReturn($message);
        
        $sut = new Sut($twilioStub);
        $result = $sut->method();
    }
}

Result

Execute the tests:

vendor/bin/phpunit tests/

Please verify if this code is working for you also. If not, please tell me what composer library versions you use. Consider adding this info to your question composer show -i.

The result should be:

PHPUnit 9.5.21 #StandWithUkraine

.                                                                   1 / 1 (100%)

Time: 00:00.006, Memory: 4.00 MB

OK (1 test, 1 assertion)

I expect you somehow call retrieveLastTextFromBot on something else than the mock object

If the code above is working, I suspect you somehow call the method retrieveLastTextFromBot on the production code, instead of the mock object or you might execute the method twice from a different location.

To find out what is really happening add the following code to retrieveLastTextFromBot (in your production code):

public function retrieveLastTextFromBot()
{
    var_dump(debug_backtrace()[0]['file']); # This line
    var_dump(debug_backtrace()[0]['line']); # This line
    exit; # This line

    $messages = $this->twilioClient->messages->read([
        'to' => TwilioDefinitions::getToNumber(),
        'from' => getenv('TWILIO_NUMBER'),
    ], 1);
    if (count($messages) !== 1) {
         throw new NoExtantTextException('No previous message from ' . getenv('TWILIO_NUMBER') . ' to ' . TwilioDefinitions::getToNumber());
    }
    return $messages[0];
}

If it returns the file where the SUT (System under test) is located, you now know for sure you don't have executed the method on the mock object. You might see a different file, in that case, check your code to see from where you execute it.

Check your tests to see what instance is passed in the constructor. If otherwise, let me know what the result of the debug_backtrace() is and consider updating your question with the results so we can continue to help, since not all code is provided it might be needed to help your further.

If non of the previous explained tests gave you enough information, please consider adding a temporarily github repository with your code and maybe if possible some test environment credentials.

Upvotes: 2

Related Questions