【问题标题】:Laravel mock external serviceLaravel 模拟外部服务
【发布时间】: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 对象没有按预期模拟,但我不知道为什么。在这种情况下,我是否正确应用了对象模拟?

【问题讨论】:

    标签: php laravel unit-testing phpunit mockery


    【解决方案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,并添加一个构造函数开始。但我同意它会更干净。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-23
      • 2016-04-15
      • 2019-09-30
      • 1970-01-01
      • 1970-01-01
      • 2016-03-06
      相关资源
      最近更新 更多