Laravel 模拟外部服务

Posted

技术标签:

【中文标题】Laravel 模拟外部服务【英文标题】:Laravel mock external service 【发布时间】:2021-10-12 01:19:49 【问题描述】:

我有一些类可以创建用户“集成”并使用外部 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 IntegrationApi

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

        return false; // just for example
    

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

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 对象没有按预期模拟,但我不知道为什么。在这种情况下,我是否正确应用了对象模拟?

【问题讨论】:

【参考方案1】:

您需要了解依赖注入和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']);

【讨论】:

【参考方案2】:

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

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

class IntegrationService

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

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


正如 Maksim 在 cmets 中指出的那样,构造函数注入是避免使用 app() 的另一种方式:

class IntegrationService

    protected $api;
    public function __construct(IntegrationApi $api)
    
        $this->api = $api;
    
    public function create(array $params)
    
        if (!$this->api->checkCredentials ...

n.b.:您不需要手动提供/定义这些参数/它们的位置来获得您的服务。如果你还在 Controller 中使用 app()/injection 请求服务,Laravel 会自动处理(使用反射)。

【讨论】:

为什么是 app()?构造函数注入是清晰而漂亮的方式 构造函数注入还需要他们用这些方法(他们应该 obv)实例化IntegrationService,并添加一个构造函数开始。但我同意它会更干净。

以上是关于Laravel 模拟外部服务的主要内容,如果未能解决你的问题,请参考以下文章

Laravel - 通过外部 API 进行身份验证

在 Laravel 中运行功能测试时如何模拟服务(或服务提供者)?

从 Laravel(NodeJS)外部推送到 Laravel 队列

laravel5 怎么把默认日志写到外部服务器

Laravel 模拟未执行

laravel csrf保护