PHPUnitでinterfaceのテストを書く方法


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 クラス図

![UML]



### 2.3 コード

```php:FormatterInterface
<?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:JsonFormatter
<?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:BuggyFormatter
<?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:FormatterInterfaceTest
<?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:JsonFormatterTest
<?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: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の単体テストを書く際には、インターフェイスについてのテストを抽象テストクラスに書き、実装クラスのテストはその抽象テストクラスを継承するようにするといいでしょう。


[suin/unit-test-patterns]: https://github.com/suin/unit-test-patterns/tree/master/src/Suin/UnitTestPatterns/TestingInterfaces
[UML]: https://raw.github.com/suin/unit-test-patterns/master/src/Suin/UnitTestPatterns/TestingInterfaces/uml.png