Unit testing Drupal static methods usage

They say static methods are hard to test. They are wrong. Static methods are easy to test, but hard to mock. Unfortunately developing software inside Drupal requires implementing several static method calls. Some of them may be avoided, like getting services, which may be easily injected as a dependencies (and that’s actually how it should be done).

Some would say, you can mock container and then set services’ mocks, but this is really wrong approach. This is not unit testing. Unit testing is about testing only the part of code you create, nothing more. This is because every unit test should have only one reason to fail and this reason should be mentioned in the test name. If your test relies on some additional code, it may fail, if someone makes changes to this additional code. You wouldn’t want to fix dozens of tests because of some little change, would you?

However in Drupal there are some static calls which may not be injected, like Drupal\user\Entity\User::load. One way to solve this problem is mock whole User class and require mock in test file. This will work if your mock will be using Drupal\user\Entity namespace, because if you require mock file in the top of your test file, autoloading mechanism won’t look for class implementation, because class will be already loaded. Unfortunately this code forces you to write too much code, like setters for users to mock, etc.

UserMock.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?php
namespace Drupal\user\Entity;

class User {
  private static $mockedUser;

  public static function load($id)
    return $mockedUser;
  }

  public static mockUser($userMock) {
    self::$mockedUser = $userMock;
  }
}

Test class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
namespace module\test\Unit;

require_once "../mocks/UserMock.php"

use Drupal\user\Entity\User;

//some additional code here

class SomeUnitTest extends UnitTestCase {

  public function testShouldReturnCurrentUser() {
    // some code here
    User::mockUser($myUserMock);
    $testedClass = new TestedClass();
    $actualUser = $testedClass->fetchCurrentUser();
    $this->assertEquals($expectedUser, $actualUser);
  }
}

There is actually better way to solve this problem: not using static calls in your class. To do so, you may create wrapper shared by objects like a service, which may be injected at will. But writting wrapper which delegates all it’s calls to a static class might be a pain, so it got me thinking and I found a nice solution to that – anonymous functions and proper constructor. So, there are some mandatory parameters which need to be passed when creating mocked object, but after them there are optional parameters with default values (or an array of them) which if set, will redefine default lambdas. And if one would like to mock a static call used in tested class, they will just pass proper anonymous function to created object. So the code will look like this:

Tested class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
// some code here
class TestedClass {

  private $userLoad;
  private $id;

  public function __construct($id, $userLoad = null) {
    $this->id = $id;
    $this->userLoad = $userLoad ?? static function($userId) {
      return User::Load($userId);
    };
  }

  public function fetchTheUser() {
  // some code here
  $user = ($this->userLoad)($someUserId);
  // some code here
  }
}

Unit test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
class SomeUnitTest extends UnitTestCase {

  public function testShouldReturnSomeUser() {
    // some code here
    $mockUserLoad = static function($userId) use ($mockedUser) {
      return $mockedUser;
    };
    $testedClass = new TestedClass("someId", $mockUserLoad);
    $actualUser = $testedClass->fetchTheUser();
    $this->assertEquals($expectedUser, $actualUser);
  }
}

And that’s it. You can safely use static calls in your code and easily mock them.