PHPUnitで静的(static)メソッドのモック

次のような静的メソッドを持つクラスがあったとします。

```php
<?php
class StaticMethods
{
    public static function func()
    {
        return "this is static method";
    }
}
```

テスト対象のコードで `StaticMethods::func()` などとクラス名がハードコードされているとユニットテストが困難です。

.

次のようにすれば静的メソッド（を持つクラス）をモック（スタブ）にすることができます（パーシャルモックはできません）。

```php
<?php
class StaticMethodsTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @param string $class
     * @param array $methods
     * @return \PHPUnit_Framework_MockObject_MockObject
     */
    protected function getStaticMock($class, array $methods = array())
    {
        $arr = explode("\\", trim($class, '\\'));
        $origShortName = array_pop($arr);
        $origNamespace = implode("\\", $arr);

        $dummyNamespace = trim('__DUMMY__\\' . $origNamespace, '\\');
        $dummyShortName = $origShortName;
        $dummyClassName = "$dummyNamespace\\$dummyShortName";

        if (class_exists($dummyClassName, false) == false) {
            $code = [];
            $code[] = "namespace $dummyNamespace;";
            $code[] = "class $dummyShortName {";
            foreach ($methods as $method) {
                $code[] = "public static function $method(){}";
            }
            $code[] = "}";
            eval(implode("\n", $code));
        }

        $mock = $this->getMock($dummyClassName, $methods);
        class_alias(get_class($mock), $class);
        return $mock;
    }

    public function test()
    {
        $mock = $this->getStaticMock('StaticMethods', ['func']);
        $mock->staticExpects($this->any())
            ->method('func')
            ->will($this->returnValue("this is mock value"))
        ;

        $this->assertEquals("this is mock value", StaticMethods::func());
    }
}
```

`StaticMethods` クラスがテストの実行時にはまだ未定義であることを前提に `getStaticMock()` の第２引数で指定された静的メソッドを持つクラスを適当な名前で定義し、そのクラスのモックを `class_alias` で本来のクラス名にエイリアスしています。

詳しくないですが Mockery は同じような原理で静的なメソッドをモックにできるらしいです。

なお `StaticMethods` をモッククラスとして定義するので、別のテストで `StaticMethods` を呼ぶと意図しない結果になります。例えば、次のようなテストケースだと test1 の実行時に `StaticMethods` がモックとして定義されるので `test2` のテストは通りません。

```php
<?php
class StaticMethodsTest extends \PHPUnit_Framework_TestCase
{
    // ...

    public function test1()
    {
        $mock = $this->getStaticMock('StaticMethods', ['func']);
        $mock->staticExpects($this->any())
            ->method('func')
            ->will($this->returnValue("this is mock value"))
        ;

        $this->assertEquals("this is mock value", StaticMethods::func());
    }

    public function test2()
    {
        $this->assertEquals("this is static method", StaticMethods::func());
        // Failed asserting that null matches expected 'this is static method'.
    }
}
```

`@runInSeparateProcess` アノテーションを使えば test1 は別プロセスになるので回避できます。

```php
<?php
class StaticMethodsTest extends \PHPUnit_Framework_TestCase
{
    // ...

    /**
     * @runInSeparateProcess
     */
    public function test1()
    {
        $mock = $this->getStaticMock('StaticMethods', ['func']);
        $mock->staticExpects($this->any())
            ->method('func')
            ->will($this->returnValue("this is mock value"))
        ;

        $this->assertEquals("this is mock value", StaticMethods::func());
    }

    public function test2()
    {
        $this->assertEquals("this is static method", StaticMethods::func());
    }
}
```

がしかし `@runInSeparateProcess` アノテーションで別プロセスにしても、テストの実行前に `StaticMethods` が定義されているとモックの作成時にクラスの多重定義になってコケます。例えば次のようなケースでは test0 で `StaticMethods` の本来のものが定義されるのでテストが通りません。

```php
<?php
class StaticMethodsTest extends \PHPUnit_Framework_TestCase
{
    // ...

    public function test0()
    {
        $this->assertEquals("this is static method", StaticMethods::func());
    }

    /**
     * @runInSeparateProcess
     */
    public function test1()
    {
        $mock = $this->getStaticMock('StaticMethods', ['func']);
        // Cannot redeclare class StaticMethods

        $mock->staticExpects($this->any())
            ->method('func')
            ->will($this->returnValue("this is mock value"))
        ;

        $this->assertEquals("this is mock value", StaticMethods::func());
    }
}
```

これは StaticMethods をロードするすべてのテストに `@runInSeparateProcess` アノテーションを付ければ解決します。

```php
<?php
class StaticMethodsTest extends \PHPUnit_Framework_TestCase
{
    // ...

    /**
     * @runInSeparateProcess
     */
    public function test0()
    {
        $this->assertEquals("this is static method", StaticMethods::func());
    }

    /**
     * @runInSeparateProcess
     */
    public function test1()
    {
        $mock = $this->getStaticMock('StaticMethods', ['func']);
        $mock->staticExpects($this->any())
            ->method('func')
            ->will($this->returnValue("this is mock value"))
        ;

        $this->assertEquals("this is mock value", StaticMethods::func());
    }
}
```

あるいは `@preserveGlobalState disabled` とアノテーションを書いておけば `@runInSeparateProcess` で **分離プロセスでテストを実行するときに元のプロセスで読まれていたスクリプトを分離プロセスでも自動的に読む** という機能が無効になるので解決します。

```php
<?php
class StaticMethodsTest extends \PHPUnit_Framework_TestCase
{
    // ...

    public function test0()
    {
        $this->assertEquals("this is static method", StaticMethods::func());
    }

    /**
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     */
    public function test1()
    {
        $mock = $this->getStaticMock('StaticMethods', ['func']);
        $mock->staticExpects($this->any())
            ->method('func')
            ->will($this->returnValue("this is mock value"))
        ;

        $this->assertEquals("this is mock value", StaticMethods::func());
    }
}
```

ただし PHPUnit の bootstrap で指定したスクリプトも読まれなくなるので、bootstrap で `require_once 'PHPUnit/Framework/Assert/Functions.php'` などとしてアサーションで `$this` を付けなくても良いようにしていたりする場合はテストの中で読む必要があります。

```php
<?php
class StaticMethodsTest extends \PHPUnit_Framework_TestCase
{
    // ...

    public function test0()
    {
        $this->assertEquals("this is static method", StaticMethods::func());
    }

    /**
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     */
    public function test1()
    {
        require_once 'PHPUnit/Framework/Assert/Functions.php';

        $mock = $this->getStaticMock('StaticMethods', ['func']);
        $mock->staticExpects(any())
            ->method('func')
            ->will(returnValue("this is mock value"))
        ;

        assertEquals("this is mock value", StaticMethods::func());
    }
}
```