Using dependency injection container from Symfony and PHPUnit for testing in Drupal

June 18, 2021

Some time ago we had a project using Drupal 7 that among other requirements, it required to demonstrate automatic testing functionality. Drupal brings a module called SimpleTest testing. By doing testing with SimpleTest, we realized it was not going to be sufficient to meet project requirements.

By design, Drupal 7 source code is written based on procedures. The "Drupal Way" which is the way or style of writing driven by the community of Drupal code, follow the same line. This presents a problem since the functions in Drupal 7 are linked with each other and it is very difficult to unit test this type of code.

To solve our problem we decided to do three things:

  • Working with object-oriented hooks within Drupal code.
  • Using a dependency injection container to pass services required by our objects.
  • Use PHPUnit for our tests.

We decided to use PHPUnit for some reasons, including:

  • How stable, robust, popular and documented is this library.
  • Drupal 8 uses this same library for unit testing your code.

To perform unit tests, you must use another type of architecture in the code. It should be used to encapsulate functionality objects that can be tested discreetly.

Our system makes requests to a remote server that handles the data needed to deploy the user the information required.

Usually a callback to implement a request for a list of properties would look like this:


function my_module_get_properties_from_remote($person_id) {
  $client = new Client();

  $params = array(
    'query' => array(
      'owner_id' => $this->person_id;
    )
  );

  $response =
    $client->request('GET', 'http://example.com/person/properties', $params);

  return $response;
}

This presents a particularity, that the function is dependent on a client Guzzle. To use OOP must make changes to implement an object that does what we want:


use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class Owner {
  private $person_id = NULL;
  private $reponse = NULL;

  public function __contruct($person_id) {
    $this->person_id = $person_id;
  }

  public function getPropertyList() {
    $client = new Client();

    $params = array(
      'query' => array(
        'owner_id' => $this->person_id;
      )
    );

    $response =
      $client->request('GET', 'http://example.com/person/properties', $params);

    return $response;
  }
}

The function uses the object would be rewritten like this:


function my_module_get_properties_from_remote($person_id) {
  $owner = new Owner($person_id);

  return $owner->getPropertyList();
}

With this we gain the ability to test the Owner object and more specifically getPropertyList prove that the method is returning what is expected, which is the core of this function.


class OwnerTest extends PHPUnit_Framework_TestCase {

  public function testCanGetListOfProperties() {
    $owner = new Owner('3345');

    $properties = $owner->getPropertyList();

    $this->assertCount(3, $properties);
    $this->assertContains('21 JumpStreet', $properties);
  }

}

Now, we have a dependence connection Guzzle. But what if we do not want / we can use that connection to our automatic unit testing? What if we use a connection to a test server? What if the functionality we need is not listed on the remote server and must use mocks? This is achieved by injecting a connection to the object.


use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class Owner {
  private $person_id = NULL;
  private $reponse = NULL;
  private $client = NULL;

  public function __contruct($person_id, Client $client) {
    $this->person_id = $person_id;
    $this->client = $client
  }

  public function getPropertyList() {
    $params = array(
      'query' => array(
        'owner_id' => $this->person_id;
      )
    );

    $response =
      $this->client->request('GET', 'http://example.com/person/properties', $params);

    return $response;
  }
}

Now we can test with a mock connection, for example, which provides Guzzle.


class OwnerTest extends PHPUnit_Framework_TestCase {
  private $mockResponses = NULL;
  private $handler = NULL;
  private $client = NULL;

  function setUp() {
    parent::setUp();

    $this->mockResponses = new MockHandler([
      new Response(200, ['Content-Type' => 'application/json'], $some_json_listing),
    ]);

    $this->handler = HandlerStack::create($this->mockResponses);
    $this->client = new Client(['handler' => $this->handler]);
  }

  public function testCanGetListOfProperties() {
    $owner = new Owner('3345', $this->client);

    $properties = $owner->getPropertyList();

    $this->assertCount(3, $properties);
    $this->assertContains('21 JumpStreet', $properties);
  }

}

We have tests running but how do we pass the connection to an object within a function X in our application? This is where comes into play dependency injection container. In our case, we chose to use the module inject, which uses the Symfony container, which is also the one that Drupal 8 uses.

In our module, we can write a function like this to get a service container:


function my_module_service_container() {
  static $container;

  if (is_null($container)) {
    $container = new Symfony\Component\DependencyInjection\ContainerBuilder();
  }

  if (!$container->has('remote_service')) {
    $remote_settings = variable_get('remote_settings');
    $guzzle_options = array(
      'uri' => $remote_settings['default']['endpoint'],
      'auth' => array(
        $remote_settings['default']['user'],
        $remote_settings['default']['password']
      ),
      'connect_timeout' => 30,
      'timeout' => 30,
    );

    $container->setParameter('remote.settings', $guzzle_options);
    $container
      ->register('remote_service', '\GuzzleHttp\Client')
      ->addArgument('% remote.settings%');
  }

  return $container;
}

A call to the module where it is declared the container and ready, We have injected dependency.


function my_module_get_properties_from_remote($person_id) {

  $container = my_module_service_container();
  $owner = new Owner($person_id, $container->get('remote_service'));

  return $owner->getPropertyList();
}

¡Boom! Done.

In conclusion, our strategy consists of several tools that allow us to execute repeatable unit tests that can be automated. Using dependency injection, PHPUnit and OOP allows us to achieve this goal of automating our tests.