LoginSignup
40
38

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-07-12

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

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

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

.

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

<?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() の第2引数で指定された静的メソッドを持つクラスを適当な名前で定義し、そのクラスのモックを class_alias で本来のクラス名にエイリアスしています。

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

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

<?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
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
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
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
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
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());
    }
}
40
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
38