NetBeans 上で PHPUnit が使えるのは非常に便利だが、その際にNamespace を用いているクラスの場合、名前解決に結構クセがありハマったのでメモ。
以下いくつか注意点を列挙。
PHPUnitフレームワーククラス自体の名前解決
当然ながらテストクラスはnamespace宣言により、テスト対象クラスの名前空間になっているので、継承時にPHPUnit_Framework_TestCaseだけではダメで最初に"\" が必要。サイトや書籍のサンプルを試すときは要注意(テストクラス自動生成時は既に追加されている)。
namespace Hoge\Moge;
class SomeClassTest extends \PHPUnit_Framework_TestCase
{
}
クラス読み込み(require_once)
テストで使用するクラスはモックを使用する場合でも全て読み込みをする必要がある。 通常テスト対象クラスとテストクラスはディレクトリが異なるので(例えばsrcとtestsのように)、そこまでのパスを相対指定する。
namespace Hoge\Moge;
require_once __DIR__ . '/../../../src/Hoge/Fuga/AnotherClass.php';
ただし、この方法だと階層が深いと読みづらい上に、使用クラスが多くなると大変。bootstrap.phpを用意してauto_loadを実装したクラスローダークラス等を先に読み込ませた方がスマート。
その場合は、プロジェクトのプロパティ>PHPUnitカテゴリーのUse Bootstrapにチェックしてファイルを指定するか、require_onceで読み込ませる。
namespace Hoge\Moge;
require_once __DIR__ . '/../../bootstrap.php';
この方法だと使用クラスが増えた際も、その都度個別のファイルを指定する必要がなく便利。
クラスインスタンス生成
通常のクラスインスタンス生成はuseキーワードを追加することで、ショート名を用いることができる。
namespace Hoge\Moge;
require_once __DIR__ . '/../../bootstrap.php';
use Hoge\Fuga\AnotherClass;
class SomeClassTest extends \PHPUnit_Framework_TestCase
{
public function testMethod() {
$class = new AnotherClass();
}
}
モック生成時のクラス名指定
問題はモックを使用する場合で、requireで個別にファイルを読み込ませたか、クラスローダーに読み込ませたかに関わらず完全修飾名で指定する必要がある。
namespace Hoge\Moge;
require_once __DIR__ . '/../../bootstrap.php';
use Hoge\Fuga\AnotherClass;
class SomeClassTest extends \PHPUnit_Framework_TestCase
{
public function testMethodNotUseMock() {
$class = new AnotherClass();
$this->assertEquals('foo', $class->doSomething());
}
public function testMethodUseMock() {
$stub = $this->getMock('Hoge\Fuga\AnotherClass');
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo');
$this->assertEquals('foo', $stub->doSomething());
}
}
なぜか、PHPUnitに渡すクラス名の場合、名前解決は完全修飾名ではないとダメな模様。自分は、これに引っ掛かって相当時間を費やした。もしかしたら設定で指定できるのかもしれないが、とりあえずこれでも解決。
Abstract クラス
抽象クラスのモックインスタンス生成はgetMockForAbstractClassを使う(抽象クラスはそもそもgetMockではインスタンス生成できない)。その際、具象メソッドはメソッド上書きの対象にならないので注意。抽象クラス上の具象メソッドを偽装させたい時は一度その抽象クラスを継承したクラスを宣言した上で、そのクラスをgetMockで上書きする必要がある。
// AbstractClass.php
abstract class AbstractClass
{
public function notOverridable() {
return 'hoge';
}
public function concrete() {
$this->overridable();
}
public abstract function overridable();
}
-------------------------
// AbstractClassTest.php
class ConcreteClass extends Abstracrt
{
public function overridable() {
return 'fuga';
}
}
class AbstractedClassTest
{
public function testAbstractMethod() {
$stub = $this->getMockForAbstractClass('AbstractClass');
$stub->expects($this->array())
->method('overridable')
->will($this->returnValue('moge');
// abstractメソッド(overridable()) は上書き(実装)される
$this->assertEquals('moge', $stub->concrete());
}
public function testConcreteMethod() {
$stub = $this->getMockForAbstractClass('AbstractClass');
$stub->expects($this->array())
->method('notOverridable')
->will($this->returnValue('moge');
// 具象関数 notOverridable は上書きされず 'hoge'が返される(アサート発生)
$this->assertEquals('moge', $stub->notOverridable());
}
public function testOverridedConcreteMethod() {
$stub = $this->getMock('ConcreteClass');
$stub->expects($this->array())
->method('notOverridable')
->will($this->returnValue('moge');
// ようやく overridable() は上書き(実装)される
$this->assertEquals('moge', $stub->notOverridable());
}
}
もっともこのように抽象クラスの具象メソッドを偽装させたい時に、テストクラス側に抽象クラスのサブクラスを記述しなければならないのであれば、あまりgetMock系メソッドの恩恵は少ないように感じなくもないが、それでもなお各テストメソッドにおいて動的に返り値を指定できるというメリットは大きい。