PHPUnitでinterfaceの単体テストを書く方法について紹介します。
1. ポイント
PHPUnitでinterfaceのテストを書く上でのポイントは次の2点です。
- インターフェイスについてのテストを抽象テストクラスに書く
- 実装クラスのテストはその抽象テストクラスを継承する
2. 例
配列を文字列に変換するライブラリを例に、どのようにテストを書くか見て行きましょう。
2.1 どのようなライブラリか?
例に上げるのは、配列を文字列に変換する汎用的なライブラリです。 どのような形式に変換するかは、クラスによって異なります。あるクラスはJSON形式に、あるクラスはXML形式に変換します。FormatterInterface
で各クラスが実装すべきインターフェイスを定義します。JsonFormatter
クラスは FormatterInterface
を実装して、JSON形式の文字列を返します。 BuggyFormatter
クラスはバグがあるクラスです。JSON形式を返そうとしますが、バグのため文字列ではなく false
を返してしまいます。なお、このライブラリのソースコードはGitHubのsuin/unit-test-patternsに収録されています。
2.2 クラス図
2.3 コード
<?php
namespace Suin\UnitTestPatterns\TestingInterfaces;
/**
* The interface for clients who wants to format data to string.
*/
interface FormatterInterface
{
/**
* Format an array to string
*
* @param array $data
* @return string A formatted string
*/
public function format(array $data);
}
JsonFormatter
JSON形式の文字列を返す具象クラスです。
<?php
namespace Suin\UnitTestPatterns\TestingInterfaces;
/**
* JSON formatter
*/
class JsonFormatter implements FormatterInterface
{
/**
* {@inherit}
*/
public function format(array $data)
{
return json_encode($data);
}
}
FormatterInterface
は戻り値の型を string
にすることを要求していますが、 BuggyFormatter
には不具合があり、 bool
を返すようになっています。この不具合は、 FormatterInterface
のテストで検出されます。
<?php
namespace Suin\UnitTestPatterns\TestingInterfaces;
/**
* This is a example class and has a bug
*/
class BuggyFormatter implements FormatterInterface
{
/**
* {@inherit}
*/
public function format(array $data)
{
if (!is_array($data) === false) { // here is a bug
return false;
}
return json_encode($data);
}
}
2.4 テストコード
まず、インターフェイス用のテストケースを用意します。テストを書く内容としては、全ての具象クラスが遵守しなければならい事項に限定してテストを書きます。具象クラス固有のテストケースについては、インターフェイス用のテストケースには書かず、具象クラス用のテストケースに書きます。具象クラスとインターフェイスのシグネチャが合っていない場合は、テスト実行時にエラーになるので、シグネチャについてテストを書く必要はありません。インターフェイステストケースの、テストメソッドは final
キーワードを付け、具象クラスが誤ってオーバーライドしないようにします。
- 引数やコンテキスト、メソッド実行前のオブジェクトの状態
- 戻り値、例外の型、メソッド実行後のオブジェクトの状態変化のチェック
- オブジェクトの検証、論理的におかしくなっていないかなど
インターフェイス用テストケースは、抽象クラスにした上でテンプレートメソッドパターンを適用します。テンプレートメソッドは、下位クラスのテストケースが、具象クラスのオブジェクトを返すために用意します。
インターフェイス用テストケースは、PHPUnit_Framework_TestCase
を継承します。具象クラス用のテストケースはインターフェイス用テストケースを継承した上で、具象クラス固有のテストケースを書きます。
Formatterの例では、format()
メソッドについてテストします。具象クラスが配列を受け取れ、戻り値の文字列フォーマットはJSONかもしれないし、XMLかもしれないので、フォーマットの妥当性については検証せず、戻り値の方が string 型かだけにつてテストします。このテストを testContractOfFormat()
メソッドにてテストコードにします。
<?php
namespace Suin\UnitTestPatterns\TestingInterfaces\Tests;
use Suin\UnitTestPatterns\TestingInterfaces\FormatterInterface;
abstract class FormatterInterfaceTest extends \PHPUnit_Framework_TestCase
{
/**
* Return concrete formatter object
* @return FormatterInterface
*/
abstract public function getFormatter();
/**
* Tests the contract of format() method
*
* This is a test that executed commonly all concrete classes.
*/
final public function testContractOfFormat()
{
$array = array('dummy', 'values');
$formatter = $this->getFormatter();
$result = $formatter->format($array);
$this->assertInternalType(
'string',
$result,
sprintf('%s::format() MUST returns string', get_class($formatter))
);
}
}
具象クラスの JsonFormatter
のテストは、FormatterInterfaceTest
を継承した JsonFormatterTest
クラスを作成します。こうすることで、JsonFormatter
についても testContractOfFormat()
のテストが実行されます。JsonFormatter
固有のテストは JsonFormatterTest
クラスに書きます。以下の例では、 testFormat()
でJSONの書式についてテストしています。
<?php
namespace Suin\UnitTestPatterns\TestingInterfaces\Tests;
use Suin\UnitTestPatterns\TestingInterfaces\JsonFormatter;
class JsonFormatterTest extends FormatterInterfaceTest
{
/**
* {@inherit}
*/
public function getFormatter()
{
return new JsonFormatter();
}
/**
* This is a class owned test case.
*/
public function testFormat()
{
$data = array(
'foo' => 1,
'bar' => 2,
'baz' => 3,
);
$formatter = new JsonFormatter();
$result = $formatter->format($data);
$this->assertSame('{"foo":1,"bar":2,"baz":3}', $result);
}
}
BuggyFormatterTest
は FormatterInterfaceTest
を継承したばかり不完全な状態です。この段階でも、BuggyFormatterTest
は親クラスのテストが実行されため、インターフェイスをちゃんと実装できているかチェックすることができます。
<?php
namespace Suin\UnitTestPatterns\TestingInterfaces\Tests;
use Suin\UnitTestPatterns\TestingInterfaces\BuggyFormatter;
class BuggyFormatterTest extends FormatterInterfaceTest
{
/**
* {@inherit}
*/
public function getFormatter()
{
return new BuggyFormatter();
}
}
3. おわり
以上で解説したとおり、PHPUnitでinterfaceの単体テストを書く際には、インターフェイスについてのテストを抽象テストクラスに書き、実装クラスのテストはその抽象テストクラスを継承するようにするといいでしょう。