简体   繁体   English

如何模拟在 Laravel 功能测试中向第三方 API 发出请求的 GuzzleHttp 客户端?

[英]How do I mock a GuzzleHttp client that makes a request to a third-party API in a Laravel feature test?

In a Laravel project (Laravel 8 on PHP 8.0) I have a feature test in which I test an internal endpoint.在一个 Laravel 项目(PHP 8.0 上的 Laravel 8)中,我有一个功能测试,其中我测试了一个内部端点。 The endpoint has a Controller calls a method on a Service.端点有一个控制器调用服务上的方法。 The Service then tries to call a third-party endpoint.然后服务尝试调用第三方端点。 It is this third-party endpoint that I would like to mock.我想模拟的是这个第三方端点。 The situation currently looks like this:目前的情况是这样的:

Internal Endpoint Feature Test内部端点功能测试

public function testStoreInternalEndpointSuccessful(): void
{
    // arrange, params & headers are not important in this problem
    $params = [];
    $headers = [];

    // act
    $response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);

    // assert
    $response->assertResponseStatus(Response::HTTP_OK);
}

Internal Endpoint Controller内部端点控制器

class InternalEndpointController extends Controller
{

    public function __construct(protected InternalService $internalService)
    {
    }

    public function store(Request $request): InternalResource
    {
        $data = $this.internalService->fetchExternalData();

        return new InternalResource($data); // etc.
    }
}

Internal Service内部服务

use GuzzleHttp\ClientInterface;

class InternalService
{
    public function __construct(protected ClientInterface $client)
    {
    }
    
    public function fetchExternalData()
    {
        $response = $this->httpClient->request('GET', 'v1/external-data');
        $body = json_decode($response->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR);

        return $body;
    }
}

I have looked at Guzzle's documentation, but it seems like the MockHandler strategy requires you to execute the http request inside of the test, which is not wat I want in my test.我查看了 Guzzle 的文档,但 似乎MockHandler策略要求您在测试中执行 http 请求,这不是我在测试中想要的。 I want Guzzle's http client to be mocked and to return a custom http response that I can specify in my test.我希望 Guzzle 的 http 客户端被模拟并返回我可以在测试中指定的自定义 http 响应。 I have tried to mock Guzzle's http client like this:我试图像这样模拟 Guzzle 的 http 客户端:

public function testStoreInternalEndpointSuccessful(): void
{
    // arrange, params & headers are not important in this problem
    $params = [];
    $headers = [];

    $mock = new MockHandler([
        new GuzzleResponse(200, [], $contactResponse),
    ]);

    $handlerStack = HandlerStack::create($mock);
    $client = new Client(['handler' => $handlerStack]);

    $mock = Mockery::mock(Client::class);
    $mock
        ->shouldReceive('create')
        ->andReturn($client);

    // act
    $response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);

    // assert
    $response->assertResponseStatus(Response::HTTP_OK);
}

But the InternalService does not seem to hit this mock in the test.但是InternalService在测试中似乎没有达到这个模拟。

I have also considered and tried to use Http Fake , but it didn't work and I assume Guzzle's http client does not extend Laravel's http client.我也考虑过并尝试使用Http Fake ,但它没有用,我认为 Guzzle 的 http 客户端没有扩展 Laravel 的 http 客户端。

What would be the best way to approach this problem and mock the third-party endpoint?解决此问题并模拟第三方端点的最佳方法是什么?

Edit编辑

Inspired by this StackOverflow question , I have managed to solve this problem by injecting a Guzzle client with mocked responses into my service.这个 StackOverflow question的启发,我设法通过将带有模拟响应的 Guzzle 客户端注入到我的服务中来解决这个问题 The difference to the aforementioned StackOverflow question is that I had to use $this->app->singleton instead of $this->app->bind because my DI was configured differently:与前面提到的 StackOverflow 问题的不同之处在于我必须使用$this->app->singleton而不是$this->app->bind因为我的 DI 配置不同:

AppServiceProvider.php AppServiceProvider.php

namespace App\Providers;

use App\Service\InternalService;
use GuzzleHttp\Client;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // my app uses ->singleton instead of ->bind
        $this->app->singleton(InternalService::class, function () {
            return new InternalService(new Client([
                'base_uri' => config('app.internal.base_url'),
            ]));
        });

    }
}

Depending on your depending injection, you want to bind or singleton -ify your InternalService with a custom Guzzle http client that returns mocked responses, eg like this:根据您的依赖注入,您希望使用返回模拟响应的自定义 Guzzle http 客户端bindsingleton化您的InternalService ,例如:

public function testStoreInternalEndpointSuccessful(): void
{

    // depending on your DI configuration,
    // this could be ->bind or ->singleton
    $this->app->singleton(InternalService::class, function($app) {
        $mockResponse = json_encode([
            'data' => [
                'id' => 0,
                'name' => 'Jane Doe',
                'type' => 'External',
                'description' => 'Etc. you know the drill',
            ]
        ]);

        $mock = new GuzzleHttp\Handler\MockHandler([
            new GuzzleHttp\Psr7\Response(200, [], $mockResponse),
        ]);

        $handlerStack = GuzzleHttp\HandlerStack::create($mock);
        $client = new GuzzleHttp\Client(['handler' => $handlerStack]);

        return new InternalService($client);
    });

    // arrange, params & headers are not important in this problem
    $params = [];
    $headers = [];

    // act
    $response = $this->json('POST', '/v1/internal-endpoint', $params, $headers);

    // assert
    $response->assertResponseStatus(Response::HTTP_OK);
}

See also: Unit Testing Guzzle inside of Laravel Controller with PHPUnit另请参阅:使用 PHPUnit 在 Laravel Controller 中进行单元测试 Guzzle

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM