Mocking Static Method Calls With PHPUnit

October 25, 2011 by alex

Update Nov 1, 2011

I updated the code samples in 2 places. User::sendRegistration needed to return a value, and MockTurtle was renamed to MockProxy since it just didn’t seem funny or clever anymore. Late night naming gone awry.

Overview

PHPUnit 3.5 comes with some ability to mock static method calls. You create a new test class which can expect a given static call, and then use a staticExpects() call to set up your expectations just like with the normal instance-based expects() .

http://sebastian-bergmann.de/archives/883-Stubbing-and-Mocking-Static- Methods.html

This is all fine if you can call the static method directly, or if all the static method calls are in the same class. But say you have an instance method in one class which calls a static in another class, and you want to test that the static is called correctly? You’re sunk. Can’t be done.

This was driving me crazy tonight, so I decided to try to hack a way around the problem. I want to share what I came up with, get feedback, and see if there are better ways to do it.

Setup

Forgive this painfully contrived example. We have a User , and the User calls a RegistrationService .

<?php
//User.class.php
class User {
  public function __construct($id) {
    $this->id = $id;
  }
  public function sendRegistration() {
    return RegistrationService::processRegistration( $this->id );
  }
}
?>
<?php
//RegistrationService.class.php
class RegistrationService {
  public static function processRegistration($user_id) {
    // call some external service
    return '{'.$user_id.':"success"}';
  }
  public static function getServiceName() {
    return 'registration';
  }
}
?>

In my test, I don’t want to call the real RegistrationService::processRegistration , but I do want to verify that $user->sendRegistration() is making that call correctly.

The core of my approach is a mock RegistrationService that looks like this:

<?php
//RegistrationService.mock.php

class MockProxy { // This thing could use a better name.

  private static $mock;

  public static function setStaticExpectations($mock) {
    self::$mock = $mock;
  }

  // Any static calls we get are passed along to self::$mock. public static
  function __callStatic($name, $args) {
    return call_user_func_array(
      array(self::$mock,$name), $args
    );
  }
}

class RegistrationService extends MockProxy {}

?>

And after all that setup, this is what the test looks like

<?php
require_once 'User.class.php';

class TestCase extends PHPUnit_Framework_TestCase {

  /**
   * @runInSeparateProcess
   * @preserveGlobalState disabled
   */
  public function test_sendRegistration_calls_RegistrationService() {
    require_once 'RegistrationService.mock.php';

    $mock = $this->getMock( 'RegistrationService', array('processRegistration') );
    $mock->expects( $this->once() )
      ->method( 'processRegistration' )
      ->with( 25 )
      ->will( $this->returnValue('{mock:true}') );

    RegistrationService::setStaticExpectations($mock);

    $subject = new User( 25 );
    $this->assertEquals('{mock:true}',
    $subject->sendRegistration());
  }

  public function test_RegistrationService_reports_its_service_name() {
    require_once 'RegistrationService.class.php';
    $this->assertEquals('registration', RegistrationService::getServiceName());
  }
}
?>

In the first test, any static methods calls made to the mock RegistrationService get passed along to the mock we supplied with setStaticExpectations . Note that these are normal instance-based expectations, not static ones. That’s a little counter-intuitive, but if you follow the code it makes sense.

The second test has nothing to do with User, and really doesn’t belong in this suite at all. I include it to show that you can use a mock in one test, and invoke the real un-mocked class in another test. You can find an explanation of @runInSeparateProcess and @preserveGlobalState in http://matthewturland.com/2010/08/19/process-isolation-in-phpunit/. As the first one implies, it means that this particular test will be run in its own process. This is necessary since we’re dealing with 2 different classes both named RegistrationService .

OUTPUT

alex@turnip:~/Code$ phpunit UserTest.php

PHPUnit 3.5.14 by Sebastian Bergmann.

..

Time: 1 second, Memory: 5.50Mb

OK (2 tests, 2 assertions)

and if I break the first test on purpose, the error message is clear and easy to follow. That’s one metric I was worried this approach wasn’t going to do well on, but it seems just fine in this case at least.

alex@turnip:~/Code$ phpunit UserTest.php

PHPUnit 3.5.14 by Sebastian Bergmann.

F.

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) TestCase::test_sendRegistration_calls_RegistrationService Failed asserting
that  matches expected .

/Users/alex/Code/RegistrationService.mock.php:15
/Users/alex/Code/User.class.php:9 /Users/alex/Code/User.class.php:9
/Users/alex/Code/UserTest.php:24

FAILURES! Tests: 2, Assertions: 1, Failures: 1.

THOUGHTS?

So… feedback? Are there better ways to do this, and my Google-fu was just too weak tonight? How could I improve this approach?

Let me say right off the bat that I’m really really really not interested in “statics are bad, you should refactor your code to not use them” kinds of responses.

I actually tend to agree, and if I’m creating a project from scratch I tend to follow that advice. But how many projects do you work on which are entirely your own design and your own code? In my world, the count is 0. I expect to use 3rd-party libraries. I can’t control their APIs, but I don’t feel like that fact should prevent me from doing comprehensive testing of how I use those APIs.

Actually, I think we’d have the same problem even if User were using an instance of RegistrationService instead of calling a static, and it could be solved in essentially the same way.

But again that could just be me looking at the problem wrong. It seems like so much of testing is about habits. I really learned my habits doing Rails, and the kinds of stuff you can do with Mocha just aren’t possible in PHP. That doesn’t mean good testing isn’t possible - it just means that the habits & intuition I’ve built up over the years aren’t serving me well when I try testing PHP. I’m keen to learn, so fill me in!

FINALLY, SOME LOVE

Thanks to Sebastian Bergmann and everyone else who have contributed to PHPUnit. You make the best testing tools for PHP, and if I grumble about limitations here & there it should not be interpreted as “PHPUnit sucks, look what it can’t do”. I hate that kind of attitude, and I appreciate all the work you’ve done for all of us!

Have a good night! I think I did. :)

☙ ☙ ☙