Mocking Entities and Services with PHPUnit and Mocks
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:
- Create the class of service;
- Create a new container to associate the class with the service name;
- Define the container created for later use of the tests related to the service;
- Create random numbering for the id of the class to be mock;
- Create mock class related a first class using service;
- Create the mock that makes it possible to implement the load of getStorage from EntityTypeManagerInterface;
- Create the mock of EntityTypeManagerInterface that lets you implement getStorage;
- 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
You can:
- Log in, click Edit, and edit this page
- Log in, click Discuss, update the Page status value, and suggest an improvement
- Log in and create a Documentation issue with your suggestion