22
20

More than 5 years have passed since last update.

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

Posted at

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 コード

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形式の文字列を返す具象クラスです。

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 のテストで検出されます。

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() メソッドにてテストコードにします。

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の書式についてテストしています。

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);
    }
}

BuggyFormatterTestFormatterInterfaceTest を継承したばかり不完全な状態です。この段階でも、BuggyFormatterTest は親クラスのテストが実行されため、インターフェイスをちゃんと実装できているかチェックすることができます。

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

22
20
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
22
20