Unit testing more complicated Drupal classes

Last updated on
7 January 2017

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 which 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;
    
    class TestModuleService {
    
      /**
       * 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.

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.