2

I am working on a custom module that from a controller calls Elastic search and returns a search result. I am trying to write a simple Functional Php Unit test just to make sure the results are displaying correctly.

Here is a simplified version of my funtional php unit test code:

public function testSearchResulsShowUp() {
  $account = $this->drupalCreateUser(['access content']);
  $this->drupalLogin($account);
  $this->drupalGet('/search/term1');
  $this->assertResponse(200);
  $this->assertText('Term 1');
}

Here is a simplified version of my controller:

/**
 * Search controller.
 */
class SearchController extends ControllerBase {

  public function getContent(search) {
    $client = Client::getClient();

    // Client connects to Elastic search and then returns
    // a result.

    ...
    ...

    // Will return Term 1
  }

}

Here is a simplified version of my Client class:

/**
 * Class Client.
 */
class Client {

  /**
   * Get client.
   */
  public static function getClient() {
    $cluster_id = \Drupal::state()->get('elasticsearch_connector_get_default_connector');
    $elasticsearchCluster = \Drupal::entityManager()->getStorage('elasticsearch_cluster')->load($cluster_id);
    $clientManager = \Drupal::service('elasticsearch_connector.client_manager');
    $client = $clientManager->getClientForCluster($elasticsearchCluster);
    return $client;
  }
}

I would like to be able to somehow mock my getClient method so when it is called in the Controller or anywhere going forward it would call the mock instead of the original method. I know that if this was a true Unit test we would be able to do it but since it is a functional test I am not sure how to do it.

  • 1
    Before you can do any effective mocking, you should be using dependency injection in your classes. That would be one step, because you could mock dependencies as constructor args and interact with the container in the test. – Kevin Feb 8 at 14:17
  • @Kevin so say I move the client as a dependency injection in my Controller. How would I be able to alter the getClient() method when visiting the page like this $this->drupalGet('/search/term1');? – albertski Feb 8 at 15:53
  • One way to do that, you could do new MyController($mocked_client, ...other deps from container));. Then call your controller method that responds at that route. You can also set the container to a class for the mocked client (though I have not been able to get that to work). I tested a controller from a kernel test because I just needed to see what the controller returned. – Kevin Feb 8 at 16:02
  • Just to confirm. I would run something like this: new MyController($mocked_client) and then run $this->drupalGet('/search/term1'); It will use my mocked version? – albertski Feb 8 at 16:04
2

Since the request your test is making spins up a completely new PHP process to deliver the contents of that request, nothing you do in-memory during the execution of the test will be reflected in the new HTTP request process.

Fortunately there are some techniques to create HTTP clients that can be used inside functional tests that span longer than a single request. The technique involves creating a test module, which replaces the HTTP client in the container, for all requests that happen while the module is enabled.

Your test may then:

  1. Switch the module on during setup.
  2. Use the new HTTP client in HTTP requests during the lifetime of the test.
  3. Switch the module off, if you're required to clean-up.

The key parts of a test module like this are:

my_module/tests/modules/my_module_http_client/services.yml

services:
  my_module_http_client.http_client:
    decorates: 'http_client'
    class: 'Drupal\my_module_http_client\MockHttpClient'

my_module/tests/modules/my_module_http_client/src/TestHttpClient

<?php

namespace Drupal\my_module_http_client;

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;

/**
 * Test HTTP client for the air quality test.
 */
class TestHttpClient extends Client {

  /**
   * {@inheritdoc}
   */
  public function get($uri, array $options = []) {
    return new Response(200, ['type' => 'text/xml'], file_get_contents(__DIR__ . '/some-test-fixture.xml'));
  }

}

Of course in the body of ::get, you can program any logic you like. A common pattern is to use the state API to allow the test to specify a behavior for the client. An example might be something like, setting which response fixture to return.

These kind of functional tests are really valuable to ensure your whole system is working end to end. I would however caveat this with saying, it is also valuable to identify units in your controller which can be tested in isolation. Unit tests can often test a lot more scenarios at a might higher speed than a fully bootstrapped implementation might be able to.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.