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:
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); }
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. } }
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?
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:
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'), ])); }); } }
Depending on your dependency injection, you want to
bind
orsingleton
yourInternalService
with a custom Guzzle http client that returns a mocked response, e.g. like this:See also: Unit testing inside a Laravel controller using PHPUnit