How to simulate a request to a third-party API using the GuzzleHttp client in a Laravel functional test?
P粉842215006
P粉842215006 2023-11-09 11:42:58
0
1
678

In a Laravel project (Laravel 8 on PHP 8.0), I have a functional test in which an internal endpoint is tested. The endpoint has a controller that calls a method on the service. The service then attempts to call the third-party endpoint. It is this third party endpoint that I want to simulate. The current situation is as follows:

Internal endpoint functional testing

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 Services

use GuzzleHttpClientInterface;

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've looked at Guzzle's documentation, but it seems that the MockHandler strategy requires you to perform http requests in your tests, which is not what I want in my tests. I wish to mock Guzzle's http client and return a custom http response that I can specify in my test. I try to emulate Guzzle's http client like this:

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 InternalService doesn't seem to hit this mock in testing.

I also thought about and tried using Http Fake, but it didn't work, I think Guzzle's http client doesn't extend Laravel's http client.

What is the best way to solve this problem and mock the third party endpoint?

edit

Inspired by this StackOverflow question, I successfully solved this problem by injecting a Guzzle client with mocked responses into my service. The difference from the above StackOverflow question is that I had to use $this->app->singleton instead of $this->app->bind because of my DI configuration different:

AppServiceProvider.php

namespace AppProviders;

use AppServiceInternalService;
use GuzzleHttpClient;
use IlluminateSupportServiceProvider;

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

    }
}


P粉842215006
P粉842215006

reply all(1)
P粉617597173

Depending on your dependency injection, you want to bind or singleton your InternalService with a custom Guzzle http client that returns a mocked response, e.g. like this:

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 inside a Laravel controller using PHPUnit

Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template