Skip to main content

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 /

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:

  1. 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
  2. Then it's creating a new mock handler, wired to respond with the given responses
  3. Then it's using the HandlerStack's create factory method to create a new handler stack with this handler
  4. Then it's adding a new history middleware, using the $this->history variable to store the request/response history
  5. Next, we're pushing the history middleware into the handler stack
  6. Then we're creating a new client with the given handler stack
  7. 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.