次のような静的メソッドを持つクラスがあったとします。
<?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());
}
}