Unit testing more complicated Drupal classes

Last updated on
21 June 2022

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

Dealing with global function calls

If a function (for example drupal_get_path()) is called then you have three choices:

  1. (Preferred method) Move the function call into a method of the class, override the class with a test class that overrides this method and use this version in your test. For example, imagine the following class is being tested:
     
    namespace Drupal\module_service;
    
    class ModuleService {
      public function getModulePath($moduleName) {
        return drupal_get_path('module', $moduleName);
      }
    }
    

    And imagine the following test class:
     

    namespace Drupal\Tests\module_service;
    
    use Drupal\module_service\ModuleService;
    
    class ModuleServiceTest {
      function testGetModulePath() {
        $service = new ModuleService();
        $module_path = $service->getModulePath('some_module');
    
        // Do some assertions (not shown)
      }
    }
    

    When running this test, PHPUnit will throw the following error:

    Error: Call to undefined function Drupal\module_service\drupal_get_path()

    This is because drupal_get_path() is a global function, and global functions do not exist in the testing environment.

    To get around this, the first step is to move the call to drupal_get_path() into a protected function (named getDrupalPath() below) that calls drupal_get_path():
     

    namespace Drupal\module_service;
    
    class ModuleService {
      public function getModulePath($moduleName) {
        return $this->getDrupalPath($moduleName);
      }
    
      protected function getDrupalPath($moduleName) {
        return drupal_get_path('module', $moduleName);
      }
    }
    

    The next step is to create a new testing class (called TestModuleService below) that extends the original class (ModuleService). This new class goes in the same namespace and folder as the testing class. Override the function that calls the global function (in this case, overriding getDrupalPath()). The overriding function should return a dummy value that can be used in the assertions in your tests.
     

    namespace Drupal\Tests\module_service;
    
    use Drupal\module_service\ModuleService;
    
    class TestModuleService extends ModuleService {
    
      /**
       * Override parent::getDrupalPath()
       * Do NOT call parent::getDrupalPath() inside this function
       * or you will receive the original error
       */
      protected function getDrupalPath($moduleName) {
        return 'fake/path/for/testing';
      }
    }

    Finally, the test class is altered to test against the new class (TestModuleService) instead of the original class (ModuleService)
     

    namespace Drupal\Tests\module_service;
    
    use Drupal\Tests\module_service\TestModuleService;
    
    class ModuleServiceTest {
      function testGetModulePath() {
        $service = new TestModuleService();
        $module_path = $service->getModulePath('some_module');
    
        // $module_path now equals fake/path/for/testing which has
        // been returned from TestModuleService
      }
    }
  2. Instead of namespace Drupal\foo\tests; use namespace Drupal\foo\tests { } around the class, followed by example.
    namespace {
      if (!function_exists('foo')) {
        function foo() {}
      }
    }
    

    Note this is a very dangerous practice because if two classes define the same function this way with different returning value then the results will vary depending on the order of the test call. Strive for returning some empty value (array, NULL, '') that makes sense for this function.

  3. You also can add a property / getter / setter for the function name and then the method being tested runs call_user_func_array($this->getSomeFunctionName(), $args) and the test can override the function name.
     
  4. Last but not least, you can mock a global function by adding fixtures in your test module. Let's say that you need to mock base_path().
    1. Simple go add a fixture in your testing src
    2. Add a mock for function, for example, you can define like above:
    3. Simply use require_once on your setup and you get your mocked function available and replaceable only in the test you are requiring. Make sure to use function_exists() to check if the function you need to test isn't already available.
    /**
     * Overrides global function if not exists.
     *
     * @return string
     *   Base path mocked.
     */
    if (!function_exists('base_path')) {
     function base_path() {
        return '/';
      }
    }
    

Mocking properties

Property values can be added to mock objects by mocking the __get() magic method:

// Create mock object.
$mock_field_item_list = $this->getMockBuilder(FieldItemList::class)
  ->disableOriginalConstructor()
  ->getMock();
// Add mock method.
$mock_field_item_list->expects($this->any())
  ->method('METHOD_NAME')
  ->willReturn('METHOD_RETURN_VALUE');
// Add mock property value.
$mock_field_item_list->expects($this->any())
  ->method('__get')
  ->with('PROPERTY_NAME')
  ->willReturn('PROPERTY_VALUE');

Dealing with Drupal::foo() calls

You can either apply 1. from above as well or you can build your own container with mock versions of the services requested. Don't forget to set the container to an empty ContainerBuilder in tearDown. For Example, this test sets a mock entity.manager.

When too much mocking seems to be necessary

When trying to test a single method, the constructor and the method itself might unleash a whole chain of method calls and Drupal service calls and it seems the test will drown in a sea of mocking. This is a code smell and you really should try to fix it. This is not a technique to be used when you are writing a new class but sometimes fixing this is not possible/feasible -- fixing a small bug on an entity class does not warrant rewriting the entity class. Instead, completely kill every method but the one being tested:

    $methods = get_class_methods('Drupal\comment\Entity\Comment');
    unset($methods[array_search('preSave', $methods)]);
    $comment = $this->getMockBuilder('Drupal\comment\Entity\Comment')
      ->disableOriginalConstructor()
      ->setMethods($methods)
      ->getMock();

This will create a version of the comment entity class with every method returning NULL except preSave. If NULL is not adequate, then just add an expectation:

    $comment->expects($this->once())
      ->method('isNew')
      ->will($this->returnValue(TRUE));

As discussed in the function calls section, it is best to move function calls into a method and then override the method -- instead of writing a test class, the technique described here can be used as well.

Mocking values in the settings.php during tests

In Unit and Kernel tests, you can mock the Settings object, and you should ensure that code that relies on values in settings.php makes use of this.

$site_settings = [
  'http_services_api' => [
    'auth_services' => [
      'title' => 'Auth Services',
      'config' => [
        'base_uri' => 'https://demo.api-platform.com/auth'
      ],
    ],
    'resource_services' => [
      'title' => 'Resource Services',
      'config' => [
        'base_uri' => 'https://demo.api-platform.com/books'
      ],
    ],
  ],
];

new Settings($site_settings);

In functional tests, use the following:

$settings['settings']['my_settings_property'] = (object) [
  'value' => 'my_value',
  'required' => TRUE,
];

$this->writeSettings($settings);

Help improve this page

Page status: Needs review

You can: