Skip to main content

Mocking Network Requests in Drupal 8 Kernel Tests

Back-end Development
Drupal

To me, a good use case for writing tests when developing Drupal 8 modules is handling the many possibilities that come as a result of making requests to an external service.  But because this is an external service we don't have control over many variables like...

  • Uptime
  • Request Limits
  • Security provisions, like VPN or IP whitlisting
  • Request speed

In the past, items like these have turned me off of any attempt to write tests in this situation.  I knew mocking the network requests was probably the way to go, but it seemed daunting, and there weren't many resources for how it is accomplished. 

This article will talk a bit about just the networking aspect of writing tests.  If you are interested in learning more about the whole process of writing kernel tests in Drupal 8, see another post I created: Kernel testing custom modules in Drupal 8

Mocking Tools

Once the familiar term 'Mocking' comes into play, I start to get nervous.  I'm really not interested in constructing a fake world that would require me to be constantly developing in order to mirror the real-world.  So I get it if you aren't stoked about jumping in, but let me show you one way I've gone about it that I think keeps things simple.

Included with Drupal 8 core development tools are two options for mocking objects.  There is PHPUnit's built-in method by constructing Mock objects.  But there is also the Prophecy library.  (Drupal has a very short documentation page as well)  To me, Prophecy greatly simplifies the process of object mocking, and I really like the simplicity it offers for mocking network requests. 

Code Organization

The feature we will be using with Prophecy is the ability to define when the return values are for a given method call (for specific arguments).  To make this work for network calls, we want a class that encapsulates these calls, and little more.  We want a method to accept the parameters that will be sent to the service, and return the expected result. 

Let's use a login method call as an example.  And say that we have a NetworkService class that implements an interface that looks like this.

interface NetworkServiceInterface {

  /**
   * Authenticates user credentials.
   *
   * @param string $user
   *   The user.
   * @param string $password
   *   The password.
   */
  public function login($user, $password);

}

In our test class, we will create a method that handles the creation of these mock requests. The snippet below assumes you are implementing KernelTestBase to have access to the prophesize() method.

protected function setUpNetworkRequests() {
  $prophecy = $this->prophesize(NetworkServiceInterface::class);
  $prophecy->login('mbopp@rapiddg.com', 'test')->willReturn([
    "code" => 201,
    "result" => [
      "UserId" => "7b2ff57a-b64e-4c9c-8843-18df60ca5a16",
      "Token" => "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODYzNjg1MDMsInR5cGUiOiJHYXJyaXNvbkRlbnRhbFdlYiIsInVzZXJJZCI6IjdiMmZmNTdhLWI2NGUtNGM5Yy04ODQzLTE4ZGY2MGNhNWExNiIsInJvbGVOYW1lcyI6W10sInRva2VuVHlwZSI6MH0.9XCvFSmxaml7wbLF6jX0QS49qoRGjCsQwwBg2Xpc-nw",
      "ExpiredOn" => "2020-04-08T17:55:03Z",
    ],
  ]);
  $prophecy->login('mbopp@rapiddg.com', 'badpass')->willReturn([
    "code" => 401,
    "result" => [
      "RetryLeft" => 4,
      "Message" => "Invalid password for given identifier: mbopp@rapiddg.com. Number of retry left: 4",
      "Code" => 1004,
    ],
  ]);
  $prophecy->login('mbopp@not.com', 'test')->willReturn([
    "code" => 404,
    "result" => [
      "Message" => "No user found with the given identifier: mbopp@not.com",
      "Code" => 1001,
    ],
  ]);
  return $prophecy->reveal();
}

The reveal() method of a Prophecy object turns it into the reflected class, like your code will expect.  In this case, NetworkServiceInterface.

Using in Tests

With this method in place, we can use it in either the setUp() method, or in a particular test if it isn't needed for all tests in the class.

$networkMock = $this->setUpNetworkRequests();
$response = $networkMock->login('mbopp@rapiddg.com', 'test');
$this->assertEquals($response['result']['UserId'], '7b2ff57a-b64e-4c9c-8843-18df60ca5a16');
$this->assertEquals($response['code'], '201');

$response = $networkMock->login('mbopp@rapiddg.com', 'badpass');
$this->assertEquals($response['code'], '401');

Important: When creating the mock results of your network calls, and using the mocks in your tests, you must match the arguments exactly.  It makes sense if you consider that these mocks are very simply taking an expected value, or values, and returning an expected return value.  Much like a switch statement would.

This code shows what it's like to call the networking service directly.  The tests in this case do not tell us much.  Typically you will create the NetworkService class and pass it to another service you are testing (Dependency Injection).  It's in doing this that these network mocks become very convenient and useful.

And that's it.  If you want to see more about the whole process of running kernel tests in Drupal 8 see some other articles we have written on this topic.

Other articles in this series on testing with Drupal