Skip to main content

Mit einem Klick mehr über uns erfahren

authorized keys

Mocking static method calls in PHP

Static method calls are sometimes necessary due to an existing API or because there is no alternative like injectable services. This makes them basically impossible to mock for unit testing.

 

An example for TYPO3:

class MyClass {
  public function doSomething() {
    $text = LocalizationUtility::translate('foo');
    // Do something with $text
  }
}

The call to LocalizationUtility::translate() is static and thus there’s no way to mock it to encapsulate a unit test.

class MyClassTest extends \PHPUnit_Framework_TestCase {
  /**
   * @test
   */
  public function doesWhatIsExpected() {
    $myClassInstance = new MyClass();

    // How to ensure that the LocalizationUtility::translate() call returns a predefined text here?
    $myClassInstance->doSomething();
  }
}

This usually only leaves us one of two choices: try to mock whatever the static method call does (bad since that’s not part of our subject) or add @codeCoverageIgnore and call it a day (obviously bad, too.)

However, mocking static method calls is indeed possible by using a trait and doing the method calls slightly different. Let’s start with the trait:

/**
 * Trait for static method calls
 *
 * This is useful to make static method calls mockable in tests.
 *
 * This trait must not be used more than once in a class hierarchy,
 * otherwise endless call loops occur for parent method calls.
 * See https://bugs.php.net/bug.php?id=48770 for details.
 */
trait StaticCalling {
  /**
   * Performs a static method call
   *
   * Note that parent::class should be used instead of 'parent'
   * to refer to the actual parent class.
   *
   * @param string $classAndMethod Name of the class
   * @param string $methodName Name of the method
   * @param mixed $parameter,... Parameters to the method
   * @return mixed
   */
  protected function callStatic($className, $methodName) {
    $parameters = func_get_args();
    $parameters = array_slice($parameters, 2); // Remove $className and $methodName

    return call_user_func_array($className . '::' . $methodName, $parameters);
  }
}

The reason for using a trait here is access to protected methods which is impossible with e.g. a utility method.

Now the class above can be changed like this:

class MyClass {
  use StaticCalling;
  public function foo() {
    $text = $this->callStatic(LocalizationUtility::class, 'translate', 'foo');
    // Do something with $text
  }
}

Functionality-wise there was no change, in the end the same method call is performed. However, we are now actually able to rewrite the test and mock the method call:

class MyClassTest extends \PHPUnit_Framework_TestCase {
  /**
   * @test
   */
  public function doesWhatIsExpected() {
    $myClassInstance = $this->getMockBuilder(MyClass::class)
      ->setMethods(['callStatic'])
      ->getMock();

    $myClassInstance
      ->method('callStatic')
      ->with(LocalizationUtility::class, 'translate', 'foo')
      ->willReturn('mocked');
    
    $myClassInstance->doSomething();
  }
}

The key is that we now have a central method callStatic() we can mock in tests. Every static method call should use this method, calls to parent classes should be done with parent::class instead of 'parent' to ensure the correct parent is used.

Happy testing!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.