Laravel 模拟外部服务

Laravel mock external service

我有一些 class 创建用户“集成”并使用外部 API 检查 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
    }
}

我需要为 IntegrationService class 创建单元测试。在创建测试集成之前,我试图在我的测试中模拟 IntegrationApi class,但我的测试因该异常而失败...

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);
    }
}

似乎 IntegrationApi 对象没有像预期的那样模拟,但我不知道为什么。在这种情况下我是否正确应用了对象模拟?

当你想添加测试时你永远不能直接使用 new,它硬连接到实现 class 所以你的模拟不会被使用。

您需要使用依赖注入/服务容器:

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

这允许将实现(从 app 函数动态返回)交换到模拟对象。 如果此代码在测试上下文之外运行时没有任何绑定,Laravel 将负责调用 new


正如 Maksim 在评论中指出的那样,构造函数注入是避免使用 app():

的另一种方式
class IntegrationService
{
    protected $api;
    public function __construct(IntegrationApi $api)
    {
        $this->api = $api;
    }
    public function create(array $params)
    {
        if (!$this->api->checkCredentials ...

n.b.: 您不需要手动 provide/define 这些 args/their 职位来获得您的服务。如果您还在控制器中使用 app()/注入请求服务,Laravel 将自动处理(使用反射)。

您需要了解依赖注入和 Service Container 概念。

首先,永远不要在 Laravel 项目中使用 new 关键字 - 通过构造函数使用依赖注入:

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
    }
} 

在这种情况下测试就像

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']);
}