Testing code that makes HTTP requests in Drupal
On the surface, it may seem like code that uses Guzzle to make HTTP requests will be difficult, however thanks to Guzzle's handler and middleware APIs and Drupal's KernelTestBase, it's not that painful at all.
by
lee.rowlands
/ 2 October 2020
Preface
When you write code in Drupal 8/9 that needs to make HTTP requests, you will make use of Drupal's http_client
service. Under the hood, this is an instance of Guzzle's client.
Once you start using the service, it may feel like your code can't be tested, because you're making requests to other sites/services on the internet.
However, Guzzle has first class handling for testing HTTP requests, and with Drupal's KernelTestBase, it can be quite easy to wire up a mock handler
Pre-requisites
The code samples below assume you're using dependency injection in your code to inject an instance of the http_client
service.
Getting setup
In your kernel test, firstly you need to mock the http_client
service
The code for that looks something like this:
<?php namespace Drupal\Tests\your_module\Kernel;use Drupal\KernelTests\KernelTestBase;use GuzzleHttp\Client;use GuzzleHttp\Handler\MockHandler;use GuzzleHttp\HandlerStack;use GuzzleHttp\Middleware;use GuzzleHttp\Psr7\Response;/** * Defines a class for testing your code ✅. * * @group your_module */class YourModuleTest extends KernelTestBase { /** * {@inheritdoc} */ protected static $modules = [ 'some', 'modules', 'system', 'user', ]; /** * History of requests/responses. * * @var array */ protected $history = []; /** * Mock client. * * @var \GuzzleHttp\ClientInterface */ protected $mockClient; /** * Mocks the http-client. */ protected function mockClient(Response ...$responses) { if (!isset($this->mockClient)) { // Create a mock and queue responses. $mock = new MockHandler($responses); $handler_stack = HandlerStack::create($mock); $history = Middleware::history($this->history); $handler_stack->push($history); $this->mockClient = new Client(['handler' => $handler_stack]); } $this->container->set('http_client', $this->mockClient); } /** * Tests your module. */ public function yourModuleTest() { // ... }}
Let's breakdown what the mockClient method is doing:
- It takes a variable number or Responses as an argument. This is using the splat operator to type-hint that each argument needs to be an instance of Response, and that there is a variable number of arguments
- Then it's creating a new mock handler, wired to respond with the given responses
- Then it's using the HandlerStack's create factory method to create a new handler stack with this handler
- Then it's adding a new history middleware, using the
$this->history
variable to store the request/response history - Next, we're pushing the history middleware into the handler stack
- Then we're creating a new client with the given handler stack
- Finally, we're setting the http_client service to be our newly created client
Putting this to use
Now, we need to tell the mockClient
method what responses we expect, we do this in our test method - like so
/** * Tests your module. */ public function yourModuleTest() { $this->mockClient( new Response('200', [], Json::encode([ 'something' => 3600, 'foo' => 'bar', ])), new Response('500', [], Json::encode([ 'errors => [ 'you did something wrong', ], ])), ); // ... }
This code is wiring up the http client service to expect two requests. For the first request it will respond with a 200 status code, and a JSON-encoded body. For the second request, it will respond with a 500 and a JSON-encoded body containing some errors.
After mocking the client, you would then trigger a code-path in the system you're testing and make assertions about the return values/logic
Inspecting the requests
It's likely that you'll also want to assert that your code was making appropriate requests based on certain input parameters.
To do this, you can work with the $this->history
property, which will contain an array of request/response pairs like so:
$last_request = end($this->history)['request'];$first_response = reset($this->history)['response'];
You can access the requests made, and asset that required parameters or headers were set based on passed arguments you used when triggering the code-path you're testing.
See some examples
The Build Hooks module has been recently updated to add a fairly comprehensive test-suite. The module is designed to trigger HTTP requests against remote systems when certain events occur in Drupal. You can see example test code in that module to get a complete sense of how to use this approach in real world scenarios.
Tagged