Mocking static methods with Mockery

0

Sometimes we need to test a method from class A that contain static method calls to another class B. Static calls are always a pain for testing and the discussion about the necessity of static methods or variables can be long. So, let’s concentrate on how can we build such tests.
We will use Mockery. Mockery is a testing framework that is used to build test doubles (mocks).

The class we want to test:

namespace StaticMock\Classes\Message;

use StaticMock\Classes\Auth\StatusManager;

class Messanger {
    public function sendMessageToUser(int $userId) {
        if (StatusManager::getUserStatus($userId) == 'active') {
            return true;
        } else {
            return false;
        }
    }
}

The class where the static method is defined:

namespace StaticMock\Classes\Auth;

class StatusManager {

    public static function getUserStatus(int $userId) {
        // Theoritically, it does some stuff that affect the result
        // but to make it work we will return a fixed value
        return 'active';
    }

}

So, we go and build a first dummy test:

use PHPUnit_Framework_TestCase;
use StaticMock\Classes\Message\Messanger;

class MessangerTest extends PHPUnit_Framework_TestCase
{
    public function testMessageSending()
    {
        $messanger = new Messanger();
        $result = $messanger->sendMessageToUser(34);

        $this->assertEquals(true, $result);
    }
}

But now, we want to test the message sending method with a different return value from getUserStatus(). Mockery allow us to “create a class alias with the given classname to stdClass and are generally used to enable the mocking of public static methods. Expectations set on the new mock object which refer to static methods will be used by all static calls to this class.”. This is what we really need and this is the way we can achieve that:

use PHPUnit_Framework_TestCase;
use StaticMock\Classes\Message\Messanger;

class MessangerTest extends PHPUnit_Framework_TestCase
{
    public function testMessageSendingWithInactiveUserStatus()
    {
        $mockedStatusManager = \Mockery::mock('alias:StaticMock\Classes\Auth\StatusManager');

        $mockedStatusManager
            ->shouldReceive('getUserStatus')
            ->andReturn('inactive');

        $messanger = new Messanger();
        $result = $messanger->sendMessageToUser(34);

        $this->assertEquals(false, $result);
    }
}

The important thing to remember here is that the mocked class (here, the “StatusManager”) should not have been loaded (by this of any other test) at the time when the mocking takes place. So, if you try to run the following test class:

use PHPUnit_Framework_TestCase;
use StaticMock\Classes\Message\Messanger;

class MessangerTest extends PHPUnit_Framework_TestCase
{
    public function testMessageSending()
    {
        $messanger = new Messanger();
        $result = $messanger->sendMessageToUser(34);

        $this->assertEquals(true, $result);
    }

    public function testMessageSendingWithInactiveUserStatus()
    {
        $mockedStatusManager = \Mockery::mock('alias:StaticMock\Classes\Auth\StatusManager');

        $mockedStatusManager
            ->shouldReceive('getUserStatus')
            ->andReturn('inactive');

        $messanger = new Messanger();
        $result = $messanger->sendMessageToUser(34);

        $this->assertEquals(false, $result);
    }
}

you will probably get an error message:

Could not load mock StaticMock\Classes\Auth\StatusManager, class already exists

because the code of the mehod sendMessageToUser() in the first test will load the StatusManager class. This loading happens in the global scope and so it will persist until the testing process terminates.

Since there is no way to define the order in which test classes and methods are executed in PHPUnit, we need to isolate the alias mock from the other tests.
This can be achieved by using the @runInSeparateProcess  annotation of PHPUnit.

/**
 * @runInSeparateProcess
 */
public function testMessageSendingWithInactiveUserStatus()
{
    ....
}

Of course, you will soon realize that this is not enough. As noted in the PHPUnit documentation “By default, PHPUnit will attempt to preserve the global state from the parent process…”.
This can be solved by disabling the preservation of the global state using one more annotation:

/**
 * @runInSeparateProcess
 * @preserveGlobalState disabled
 */
public function testMessageSendingWithInactiveUserStatus()
{
    ...
}

 

Note:  The  @runInSeparateProcess  annotation in combination with @preserveGlobalState can create a side-effect by removing the autoloaders (especially if you are using PHPStorm). You will get an error message like this:

PHP Fatal error:  Class ‘PHPUnit_Util_Configuration’ not found…

If you try to run the tests from command line (using vendor/bin/phpunit ), they will probably run without problem. To resolve this issue you may have to check for existence of the autoloader during setUp(), and, if not found, re-define it.

if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
    define('PHPUNIT_COMPOSER_INSTALL', '/path/to/composer/vendors/dir/autoload.php');
}

Let’s examine one case a bit more complicated. For example, let’s say that our method to be tested uses calls not only a static method from StatusManager but also uses one of its constants:

namespace StaticMock\Classes\Message;

use StaticMock\Classes\Auth\StatusManager;

class Messanger {
    public function sendMessageToUser(int $userId) {
        if (StatusManager::getUserStatus($userId) == StatusManager::ACTIVE_USER_STATUS) {
            return true;
        } else {
            return false;
        }
    }
}

As you would expect, the mock object does not contain any constants. In this case, we can use a named mock. That means we need to define a stub class and use the nameMock() method of Mockery to tell it that the StatusManager class will be mocked using the StatusManagerStub class we are going to define.

use Mockery;
use PHPUnit_Framework_TestCase;
use StaticMock\Classes\Auth\StatusManager;
use StaticMock\Classes\Message\Messanger;

class MessangerTest extends PHPUnit_Framework_TestCase
{
    public function testMessageSending()
    {
        $messanger = new Messanger();
        $result = $messanger->sendMessageToUser(34);

        $this->assertEquals(true, $result);
    }

    /**
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     */
    public function testMessageSendingWithInactiveUserStatus()
    {
        $mockedStatusManager = Mockery::namedMock(StatusManager::class, StatusManagerStub::class);

        $mockedStatusManager
            ->shouldReceive('getUserStatus')
            ->andReturn('inactive');

        $messanger = new Messanger();
        $result = $messanger->sendMessageToUser(34);

        $this->assertEquals(false, $result);
    }
}

class StatusManagerStub
{
    const ACTIVE_USER_STATUS = 'active';
}

Problem solved!