Making Drupal 8 code testable; limiting the use of static methods and using dependency injection

Written by Charlotte Bone
19th May 2017

10 minute read

If you've been using Drupal 8 for a while, you'll know that there are multiple ways of doing the same thing. One of the things you will see in documentation and tutorials is the use of static methods. For example, if you've loaded an entity before, you might be using the method below:

 

PHP
<?php

namespace Drupal\example_module\Service;

use Drupal\user\Entity\User;

class UserService
{
  function getCurrentUserDetails(){
    $details = [];
    $current_user = \Drupal::currentUser();
    $user = User::load($current_user->id());

    if($user){
      $details = [
        'name' => $user->name->value,
        'mail' => $user->mail->value,
      ];
    }

    return $details;
  }
}

 

The problem with using static methods is that we are then unable to mock these services, so we cannot unit test easily. Just by thinking in a different way and ensuring that we inject services rather than using static methods, we can make our code easy to unit test.

One of the issues with this is that it's sometimes difficult to know what service we should be using in Drupal 8. The core\core.services.yml file lists the core services available. I recommend looking through and familiarising yourself with the most useful services. I've listed the services I have found I need to use the most below.

  • entity_type.manager - this is the service you should be using to load entities (example below)

  • entity.query - entity query service, replaces \Drupal::entityQuery

  • current_user - the current user, replaces \Drupal::currentUser()

  • user.private_tempstore - the current users private storage

  • request_stack - the current request stack

So, how do we replace the code above using dependency injection?

 

PHP
<?php

namespace Drupal\example_module\Service;

use Drupal\Core\Session\AccountProxy;
use Drupal\Core\Entity\EntityTypeManager;

class UserService
{
  /**
   * Drupal\Core\Session\AccountProxy definition.
   *
   * @var \Drupal\Core\Session\AccountProxy
   */
  protected $currentUser;

  /**
   * Drupal\Core\Entity\EntityTypeManager definition.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;

  /**
   * Constructor.
   */
  public function __construct(AccountProxy $current_user, EntityTypeManager $entity_manager) {
    $this->currentUser = $current_user;
    $this->entityTypeManager = $entity_manager;
  }

  function getCurrentUserDetails(){
    $details = [];
    $user = $this->entityTypeManager->getStorage('user')
      ->load($this->currentUser->id());

    if($user){
      $details = [
        'name' => $user->name->value,
        'mail' => $user->mail->value,
      ];
    }

    return $details;
  }
}

 

The example above is a service, so in the services.yml file you'll need to define the services to inject within the arguments section (they must begin with the @ symbol)

 

YAML
example_user_service:
  class: Drupal\example_module\Service\UserService
  arguments: ['@current_user', '@entity_type.manager']

 

If you need to inject services into a controller, you must add the additional create method inside the class to load the services. You must do the same if you're adding services to a block too but you need to ensure to also add implements ContainerFactoryPluginInterface to the class declaration.

 

PHP
/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container) {
  return new static(
    $container->get('current_user'),
    $container->get('entity_type.manager'),
  );
}

 

So, let's see how this now makes this service easy to unit test. We need to mock both the current_user and the entity_type.manager. When you're testing a class, you will usually have quite a few methods to write tests for, however each of these will require some common setup. We'll do the common setup within the setUp method, so this is usually where you'll want to setup the service mocks and instantiate your class.

When we're mocking a class, we can use get_class_methods to get all of the method names that exist within that class. This means that we can then control the return value and expecations for any method in the class in each test, without having to manually list them. You can manually input the method names if you know you'll only ever need to mock a couple, as shown in the AccountProxy mock below, we just specify the id method.

For the getStorage method, we need to return an entity storage class. To do this, we mock the ContentEntityNullStorage class and then ensure that this is returned whenever the method is called. This then allows the entity load method to be mocked.

Below is a very simple example of testing the method we created earlier. We'd ideally extend this and ensure that the data we are expecting is returned too. You will see how using dependency injection now makes our code testable, which is very important for projects where functionality is complex and/or constantly updated.

 

PHP
<?php

namespace Drupal\Tests\example_module\Unit\Service;

use Drupal\example_module\Service\UserService;
use Drupal\Tests\UnitTestCase;

/**
 * @coversDefaultClass \Drupal\example_module\Service\UserService
 * @group example_module
 */
class UserServiceTest extends UnitTestCase
{

  protected $currentUser;
  protected $entityTypeManager;
  protected $entityStorage;
  protected $userService;

  public function setUp() {
    $methods = get_class_methods('Drupal\Core\Entity\ContentEntityNullStorage');
    $this->entityStorage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityNullStorage')
      ->disableOriginalConstructor()
      ->setMethods($methods)
      ->getMock();

    $this->currentUser = $this->getMockBuilder('Drupal\Core\Session\AccountProxy')
      ->disableOriginalConstructor()
      ->setMethods(['id'])
      ->getMock();

    $methods = get_class_methods('Drupal\Core\Entity\entityTypeManager');
    $this->entityTypeManager = $this->getMockBuilder('Drupal\Core\Entity\entityTypeManager')
      ->disableOriginalConstructor()
      ->setMethods($methods)
      ->getMock();

    $this->entityTypeManager->expects($this->any())
      ->method('getStorage')
      ->willReturn($this->entityStorage);

    $this->userService = new UserService($this->currentUser, $this->entityTypeManager);
  }

  public function testGetCurrentUserDetails() {
    $this->currentUser->expects($this->once())
      ->method('id')
      ->willReturn(123);

    $this->entityStorage->expects($this->once())
      ->method('load')
      ->with(123);

    $this->userService->getCurrentUserDetails();
  }

}

 

I hope you’ve found this article useful, click here to view our other Drupal blog posts.