Mocking Entities and Services with PHPUnit and Mocks

Last updated on
18 February 2025

This documentation needs work. See "Help improve this page" in the sidebar.

Altering services in a Kernel test

If a Kernel test class implements Drupal\Core\DependencyInjection\ServiceModifierInterface, then in its alter() method it can change the definition of existing services.

Example: change the class of a service

To change the class of the EntityTypeManager service to a custom class:

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;

class MyKernelTest extends EntityKernelTestBase implements ServiceModifierInterface {

  public function alter(ContainerBuilder $container) {
    $service_definition = $container->getDefinition('entity_type.manager');
    $service_definition->setClass(TestEntityTypeManager::class);
  }

}

class TestEntityTypeManager extends EntityTypeManager {

// Additional or overridden methods here.

}

Changing the entity type manager allows you to add or override methods, as in the following examples:

Example: add a setHandler() method

The following method added to the TestEntityTypeManager class allows your test code to mock an entity handler and set it on the entity type manager:

  public function setHandler(string $entity_type_id, string $handler_type, EntityHandlerInterface $handler) {
    $this->handlers[$handler_type][$entity_type_id] = $handler;
  }

Example: alter entity type definitions

The following methods added to the TestEntityTypeManager class allow your test code to register a callback which alters entity type definitions. This keeps the alteration in the test code, rather than in a hook implementation in fixture test module.

  public function addAlter(callable $alter) {
    $this->alterers[] = $alter;

    $this->clearCachedDefinitions();
  }

  protected function alterDefinitions(&$definitions) {
    parent::alterDefinitions($definitions);

    foreach ($this->alterers as $alterer) {
      $alterer($definitions);
    }
  }

Example: alter base field definitions

The following methods added to a substituted EntityFieldManager subclass allow your test code to register a callback which alters base field definitions. This keeps the alteration in the test code, rather than in a hook implementation in fixture test module.

  public function addAlter(callable $alter) {
    $this->alterers[] = $alter;

    $this->clearCachedFieldDefinitions();
  }

  protected function buildBaseFieldDefinitions($entity_type_id) {
    $base_field_definitions = parent::buildBaseFieldDefinitions($entity_type_id);

    foreach ($this->alterers as $alterer) {
      $alterer($entity_type_id, $base_field_definitions);
    }

    return $base_field_definitions;
  }

Note that the `EntityTest` entity class used in for the entity types defined in `entity_test` module allows defining of additional base fields using state: see `EntityTest::baseFieldDefinitions`.

Decorating services in a Kernel test

Again implementing Drupal\Core\DependencyInjection\ServiceModifierInterface, the alter() method it can decorate a service:

class MyTest extends KernelTestBase implements ServiceModifierInterface {
  public function alter(ContainerBuilder $container) {
    // Rename the original service.
    $original_service_definition = $container->getDefinition('my_service');
    $container->removeDefinition('my_service');
    $container->addDefinitions([
      'my_service.original' => $original_service_definition,
    ]);

    // Add a decorated service with the original name.
    $container
      ->register('my_service', DecoratedService::class)
      ->setDecoratedService('my_service.original')
      ->addArgument(new Reference('my_service.inner'));
  }
}

class DecoratedService {

  public function __construct(protected $decoratedService) {}

  public function __call(string $name, array $arguments) {
    $this->decoratedService->$name(...$arguments);
  }

}

More details on this solution may be found here.

Example

In this case we have a service that access information from an entity that in turn relates to another entity.

A class MyServiceGitCommands refers to the service.

<?php

namespace Drupal\mymodule\MyServicesServices;

use Drupal\Core\Site\Settings;
use Drupal\ent_servidor_sites\Entity\ServidorSitesEntity;

/**
 * Serviço para utilização de comandos Git.
 */
class MyServiceGitCommands {

  /**
   * Git clone.
   *
   * @param array $data
   * @return bool|string
   */
  public function gitClone(array $data) {
    $remoteRepository = $data['repo_gitlab'];
    $branch = $data['branch'];
    $siteName = $data['site_name'];
    $idServerSite = $data['server'];
    /* @var ServidorSitesEntity $serverSite */
    $serverSite = \Drupal::entityTypeManager()->getStorage('servidor_sites')->load($idServerSite);
    $pathBaseDirSites = $serverSite->getPathBaseDirSites();
    $command = "git -C $pathBaseDirSites clone -b $branch --config core.filemode=false $remoteRepository $siteName";
    system($command, $returnInt);
    if ($returnInt !== 0) {
      return FALSE;
    }
    else {
      return TRUE;
    }
  }

This class has its route recorded in the file mymodule.services.yml.

To carry out the tests the following steps were necessary:

  1. Create the class of service;
  2. Create a new container to associate the class with the service name;
  3. Define the container created for later use of the tests related to the service;
  4. Create random numbering for the id of the class to be mock;
  5. Create mock class related a first class using service;
  6. Create the mock that makes it possible to implement the load of getStorage from EntityTypeManagerInterface;
  7. Create the mock of EntityTypeManagerInterface that lets you implement getStorage;
  8. Set the mock of EntityTypeManager into drupal container.

Write a test

1. Create the class of service:

$myServiceGitCommands = new MyServiceGitCommands();

2. Create a new container to associate the class with the service name:

$container = new ContainerBuilder();

3. Define the container created for later use of the tests related to the service:

\Drupal::setContainer($container);
$container->set('myservice.gitcommands', $serviceGitCommands);

4. Create random numbering for the id of the class to be mock:

$this->entityTypeId = $this
      ->randomMachineName();

5. Create mock class related a first class using service:

$entityServerSitesMock = $this->getMockBuilder(ServidorSitesEntityInterface::class)
      ->disableOriginalConstructor()
      ->getMock();
$entityServerSitesMock->expects($this->any())
      ->method('id')
      ->will($this->returnValue($this->entityTypeId));
$entityServerSitesMock->expects($this->any())
      /*This method exist in original class for your call use only lowercase letters*/  
      ->method('getpathbasedirsites')
      ->will($this->returnValue($arraySettings['pathBaseDirSites']));

By default, all methods of the given class are replaced with a test double that just returns NULL unless a return value is configured using will($this->returnValue()), for instance.

6. Create the mock that makes it possible to implement the load of getStorage from EntityTypeManagerInterface:

$entityStorage = $this->getMockBuilder(EntityStorageInterface::class)
      ->disableOriginalConstructor()
      ->getMock();
    $entityStorage->expects($this->any())
      ->method('load')
      ->willReturn($entityServerSitesMock);

7. Create the mock of EntityTypeManagerInterface that lets you implement getStorage:

$entityTypeManager = $this->getMockBuilder(EntityTypeManagerInterface::class)
      ->disableOriginalConstructor()
      ->getMock();
$entityTypeManager->expects($this->any())
      ->method('getstorage')
      ->willReturn($entityStorage);
  

8. Set the mock of EntityTypeManagerInterface into the Drupal container:

$container->set('entity_type.manager', $entityTypeManager);

Complete Test Class

<?php


namespace Drupal\Tests\myModule\Unit\MyServicesTest;

use Drupal\myModule\MyServices\MyServiceGitCommands;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Site\Settings;
use Drupal\ent_servidor_sites\Entity\ServidorSitesEntityInterface;
use Drupal\Tests\mymodule\Traits\FileSettingsTestTrait;
use Drupal\Tests\UnitTestCase;

/**
 * Class MyServiceGitCommandsTest.
 *
 * @coversDefaultClass \Drupal\myModule\MyModuleServices\MyServiceGitCommands
 * @package Drupal\myModule\Unit\MyServicesTest
 * @group myModule
 */
class MyServiceGitCommandsTest extends UnitTestCase {

  use FileSettingsTestTrait;

  /**
   * Attribute for creating the class representing the file settings.php.
   *
   * @var \Drupal\Core\Site\Settings
   */
  protected $settings;

  protected $arrayData;

  /**
   * The ID of the type of the entity under test.
   *
   * @var string
   */
  protected $entityTypeId;

  /**
   * Initialization of the parameters required by the test methods.
   */
  protected function setUp() {
    parent::setUp();

    $serviceGitCommands = new MyServiceGitCommands();
    
    $container = new ContainerBuilder();
    
    \Drupal::setContainer($container);
    $container->set('myservice.gitcommands', $serviceGitCommands);
   
    $this->settings = $this->createSettings();
    $arraySettings = Settings::get($this->verifyTestHost());
    $this->arrayData = $this->dataCreate($arraySettings['pathBaseDirSites']);
   
    $this->entityTypeId = $this
      ->randomMachineName();
 
    $entityServerSitesMock = $this->getMockBuilder(ServidorSitesEntityInterface::class)
      ->disableOriginalConstructor()
      ->getMock();
    $entityServerSitesMock->expects($this->any())
      ->method('id')
      ->will($this->returnValue($this->entityTypeId));
    $entityServerSitesMock->expects($this->any())
      ->method('getpathbasedirsites')
      ->will($this->returnValue($arraySettings['pathBaseDirSites']));

    $entityStorage = $this->getMockBuilder(EntityStorageInterface::class)
      ->disableOriginalConstructor()
      ->getMock();
    $entityStorage->expects($this->any())
      ->method('load')
      ->willReturn($entityServerSitesMock);
   
    $entityTypeManager = $this->getMockBuilder(EntityTypeManagerInterface::class)
      ->disableOriginalConstructor()
      ->getMock();
    $entityTypeManager->expects($this->any())
      ->method('getstorage')
      ->willReturn($entityStorage);

    $container->set('entity_type.manager', $entityTypeManager);
  }

  /**
   * Values ​​that represent the values ​​coming from form.
   */
  public function dataCreate($pathBaseDirSites) {
    return [
      'repo_gitlab' => 'git@gitlab.projects/project.git',
      'branch' => 'review',
      'site_name' => 'testeClone',
      'path_base_dir_sites' => $pathBaseDirSites,
      'path_drupal_root' => $pathBaseDirSites,
      'server' => $this->entityTypeId
    ];
  }

  /**
   * Checks if the service is created in the Drupal context.
   */
  public function testMyServiceGitCommands() {
    $this->assertNotNull(\Drupal::service('myservice.gitcommands'));
  }

  /**
   * Checks whether it is possible to clone a site from a gitLab repository.
   */
  public function testGitClone() {
    $returnBoolean = \Drupal::service('myservice.gitcommands')->gitClone($this->arrayData);
    $this->assertEquals(TRUE, $returnBoolean);
  }

  /**
   * Function performed at the end of the tests.
   *
   * Deletes the directory created during the clone site test.
   */
  public function tearDown() {
    parent::tearDown();
    $directoryTest = $this->arrayData['path_base_dir_sites'] . $this->arrayData['site_name'];
    if (file_exists($directoryTest)) {
      system('sudo rm -rf ' . $directoryTest, $returnVar);
    }
  }

}

Help improve this page

Page status: Needs work

You can: