Search code examples
phplaravelunit-testingphpunitmockery

Laravel mock external service


I have some class that creates user "integration" and check API credentials using external API:

class IntegrationService
{
    public function create(array $params)
    {
        $api = new IntegrationApi();

        if (!$api->checkCredentials($params['api_key'])) {
            throw new \Exception('Invalid credentials');
        }

        // storing to DB
        
        return 'ok'; // just for example
    }
} 

IntegrationApi class:

class IntegrationApi
{
    public function checkCredentials(string $apiKey): bool
    {
        // some external api calls

        return false; // just for example
    }
}

I need to create unit tests for IntegrationService class. I'm trying to mock IntegrationApi class in my test before creating test integration, but my test fails with that exception...

class TestIntegrationService extends TestCase
{
    public function test_create()
    {
        $service = new IntegrationService();

        $this->mock(IntegrationApi::class, function (MockInterface $mock) {
            $mock->shouldReceive('checkCredentials')->withArgs(['apiKey'])->once()->andReturnTrue();
        });

        $res = $service->create(['api_key' => '123']);

        $this->assertEquals('ok', $res);
    }
}

It seems that the IntegrationApi object was not mocked as expected, but I haven't any idea why. Have I applied object mocking correctly in this case?


Solution

  • You need to understand Dependency Injection and Service Container concepts.

    For first, never ever use new keyword in Laravel projects - use dependency injection through constructor:

    class IntegrationService
    {
        private IntegrationApi $api;
        public function __construct(IntegrationApi $api)
        {
            $this->api = $api;
        }
        public function create(array $params)
        {
            if (!$this->api->checkCredentials($params['api_key'])) {
                throw new \Exception('Invalid credentials');
            }
    
            // storing to DB
            
            return true; // never use magic strings. But in this case - void be preferred - throw exceptions on error and return nothing
        }
    } 
    

    test in this case be like

    public function setUp()
    {
       $this->mockApi = Mockery::mock(IntegrationApi::class);
       $this->service = new IntegrationService($this->mockApi);
    }
    public function testCreateOk()
    {
        $this->mockApi->shouldReceive('checkCredentials')->withArgs(['apiKey'])->once()->andReturnTrue();
        $this->assertTrue($this->service->create(['apiKey']));
    }
    
    public function testCreateError()
    {
        $this->mockApi->shouldReceive('checkCredentials')->withArgs(['apiKey'])->once()->andReturnFalse();
        $this->expectsException(Exception::class);
        $this->service->create(['apiKey']);
    }