2. PHPUnit 用のテストの書き方

Example 2.1 で、 PHP の配列操作のテストを PHPUnit 用に書く方法を示します。 この例では、PHPUnit を使ったテストを書く際の基本的な決まり事や手順を紹介します。

  1. Class という名前のクラスのテストは、ClassTest という名前のクラスに記述します。

  2. ClassTest は、(ほとんどの場合) PHPUnit\Framework\TestCase を継承します。

  3. テストは、test* という名前のパブリックメソッドとなります。

    あるいは、@test アノテーションをメソッドのコメント部で使用することで、それがテストメソッドであることを示すこともできます。

  4. テストメソッドの中で assertSame() のようなアサーションメソッド (アサーション を参照ください) を使用して、期待される値と実際の値が等しいことを確かめます。

Example 2.1 PHPUnit での配列操作のテスト
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class StackTest extends TestCase
{
    public function testPushAndPop(): void
    {
        $stack = [];
        $this->assertSame(0, count($stack));

        array_push($stack, 'foo');
        $this->assertSame('foo', $stack[count($stack)-1]);
        $this->assertSame(1, count($stack));

        $this->assertSame('foo', array_pop($stack));
        $this->assertSame(0, count($stack));
    }
}
Martin Fowler:

Whenever you are tempted to type something into a print statement or a debugger expression, write it as a test instead.

何かを print 文やデバッガの式に書きたくなったときは、 代わりにその内容をテストに書くようにするんだ。

テストの依存性

Adrian Kuhn et. al.:

Unit Tests are primarily written as a good practice to help developers identify and fix bugs, to refactor code and to serve as documentation for a unit of software under test. To achieve these benefits, unit tests ideally should cover all the possible paths in a program. One unit test usually covers one specific path in one function or method. However a test method is not necessarily an encapsulated, independent entity. Often there are implicit dependencies between test methods, hidden in the implementation scenario of a test.

ユニットテストを書くそもそもの目的は、バグの発見と修正や コードのリファクタリングを開発者がやりやすくすること。 そしてテスト対象のソフトウェアのドキュメントとしての役割を果たすことだ。 これらの目的を達成するためには、 ユニットテストがプログラム内のすべてのルートをカバーしていることが理想である。 ひとつのユニットテストがカバーするのは、 通常はひとつの関数やメソッド内の特定のルートだけとなる。 しかし、テストメソッドは必ずしもカプセル化して独立させる必要はない。 複数のテストメソッドの間に暗黙の依存性があって、 隠された実装シナリオがテストの中にあるのもよくあることだ。

PHPUnit は、テストメソッド間の依存性の明示的な宣言をサポートしています。 この依存性とは、テストメソッドが実行される順序を定義するものではありません。 プロデューサーがテストフィクスチャを作ってそのインスタンスを返し、 依存するコンシューマーがそれを受け取って利用するというものです。

  • プロデューサーとは、返り値としてテスト対象のユニットを生成するテストメソッドのこと。
  • コンシューマーとは、プロデューサーの返り値に依存するテストメソッドのこと。

Example 2.2 は、@depends アノテーションを使ってテストメソッドの依存性をあらわす例です。

Example 2.2 @depends アノテーションを使った依存性の表現
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class StackTest extends TestCase
{
    public function testEmpty(): array
    {
        $stack = [];
        $this->assertEmpty($stack);

        return $stack;
    }

    /**
     * @depends testEmpty
     */
    public function testPush(array $stack): array
    {
        array_push($stack, 'foo');
        $this->assertSame('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);

        return $stack;
    }

    /**
     * @depends testPush
     */
    public function testPop(array $stack): void
    {
        $this->assertSame('foo', array_pop($stack));
        $this->assertEmpty($stack);
    }
}

上の例では、まず最初のテスト testEmpty() で新しい配列を作り、それが空であることを確かめます。 このテストは、フィクスチャを返します。 二番目のテスト testPush()testEmpty() に依存しており、 依存するテストの結果を引数として受け取ります。 最後の testPop()testPush() に依存しています。

Note

プロデューサーの生成する戻り値は、デフォルトでは「そのままの形式」でコンシューマーに渡されます。 つまり、プロデューサーがオブジェクトを戻した場合は、そのオブジェクトへの参照がコンシューマーに渡されるということです。 参照を使う代わりに、 @depends clone を用いてディープコピーをしたり @depends shallowClone を用いてシャローコピー (PHPのキーワード clone によるコピー)をしたりすることもできます。

問題の局所化を手早く行うには、失敗したテストに目を向けやすくしたいものです。 そのため PHPUnit では、 あるテストが失敗したときにはそのテストに依存する他のテストの実行をスキップします。 テスト間の依存性を活用して問題点を見つけやすくしている例を Example 2.3 に示します。

Example 2.3 テストの依存性の活用
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DependencyFailureTest extends TestCase
{
    public function testOne(): void
    {
        $this->assertTrue(false);
    }

    /**
     * @depends testOne
     */
    public function testTwo(): void
    {
    }
}
$ phpunit --verbose DependencyFailureTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

FS

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) DependencyFailureTest::testOne
Failed asserting that false is true.

/home/sb/DependencyFailureTest.php:6

There was 1 skipped test:

1) DependencyFailureTest::testTwo
This test depends on "DependencyFailureTest::testOne" to pass.

FAILURES!
Tests: 1, Assertions: 1, Failures: 1, Skipped: 1.

ひとつのテストに複数の @depends アノテーションをつけることもできます。 PHPUnit はテストが実行される順序を変更しないので、 テストが実行されるときに確実に依存性が満たされているようにしておく必要があります。

複数の @depends アノテーションを持つテストは、 最初のプロデューサーからのフィクスチャを最初の引数、二番目のプロデューサーからのフィクスチャを二番目の引数、…… として受け取ります。 Example 2.4 を参照ください。

Example 2.4 複数の依存性を持つテスト
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class MultipleDependenciesTest extends TestCase
{
    public function testProducerFirst(): string
    {
        $this->assertTrue(true);

        return 'first';
    }

    public function testProducerSecond(): string
    {
        $this->assertTrue(true);

        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer(string $a, string $b): void
    {
        $this->assertSame('first', $a);
        $this->assertSame('second', $b);
    }
}
$ phpunit --verbose MultipleDependenciesTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

...

Time: 0 seconds, Memory: 3.25Mb

OK (3 tests, 4 assertions)

データプロバイダ

テストメソッドには任意の引数を渡すことができます。 この引数は、データプロバイダメソッド (Example 2.5additionProvider()) で指定します。使用するデータプロバイダメソッドを指定するには @dataProvider アノテーションを使用します。

データプロバイダメソッドは、public でなければなりません。また、 メソッドの返り値の型は、配列の配列あるいはオブジェクト (Iterator インターフェイスを実装しており、 反復処理の際に配列を返すもの) である必要があります。 この返り値の各要素に対して、その配列の中身を引数としてテストメソッドがコールされます。

Example 2.5 配列の配列を返すデータプロバイダの使用
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionProvider(): array
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
$ phpunit DataTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 (1, 1, 3)
Failed asserting that 2 is identical to 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.

大量のデータセットを使う場合は、デフォルトの数字を使うのではなく、各データセットに文字列の名前をつけておくと便利です。 出力もよりわかりやすくなり、テストを失敗させたデータセットの名前もわかるようになります。

Example 2.6 データプロバイダでの名前つきデータセットの使用
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionProvider(): array
    {
        return [
            'adding zeros'  => [0, 0, 0],
            'zero plus one' => [0, 1, 1],
            'one plus zero' => [1, 0, 1],
            'one plus one'  => [1, 1, 3]
        ];
    }
}
$ phpunit DataTest
PHPUnit 4.6.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set "one plus one" (1, 1, 3)
Failed asserting that 2 is identical to 3.

/home/sb/DataTest.php:9

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
Example 2.7 Iterator オブジェクトを返すデータプロバイダの使用
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionProvider(): CsvFileIterator
    {
        return new CsvFileIterator('data.csv');
    }
}
$ phpunit DataTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 ('1', '1', '3')
Failed asserting that 2 is identical to 3.

/home/sb/DataTest.php:11

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
Example 2.8 CsvFileIterator クラス
<?php
use PHPUnit\Framework\TestCase;

final class CsvFileIterator implements Iterator
{
    private $file;
    private $key = 0;
    private $current;

    public function __construct(string $file)
    {
        $this->file = fopen($file, 'r');
    }

    public function __destruct()
    {
        fclose($this->file);
    }

    public function rewind(): void
    {
        rewind($this->file);

        $this->current = fgetcsv($this->file);
        $this->key = 0;
    }

    public function valid(): bool
    {
        return !feof($this->file);
    }

    public function key(): int
    {
        return $this->key;
    }

    public function current(): array
    {
        return $this->current;
    }

    public function next(): void
    {
        $this->current = fgetcsv($this->file);

        $this->key++;
    }
}

@dataProvider で指定したメソッドと @depends で指定したテストの両方からの入力を受け取るテストの場合、 データプロバイダからの引数のほうが依存するテストからの引数より先にきます。 依存するテストからの引数は、どちらのデータセットに対しても同じになります。 Example 2.9 を参照ください。

Example 2.9 同じテストでの @depends と @dataProvider の組み合わせ
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DependencyAndDataProviderComboTest extends TestCase
{
    public function provider(): array
    {
        return [['provider1'], ['provider2']];
    }

    public function testProducerFirst(): string
    {
        $this->assertTrue(true);

        return 'first';
    }

    public function testProducerSecond(): string
    {
        $this->assertTrue(true);

        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     * @dataProvider provider
     */
    public function testConsumer(): void
    {
        $this->assertSame(
            ['provider1', 'first', 'second'],
            func_get_args()
        );
    }
}
$ phpunit --verbose DependencyAndDataProviderComboTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

...F

Time: 0 seconds, Memory: 3.50Mb

There was 1 failure:

1) DependencyAndDataProviderComboTest::testConsumer with data set #1 ('provider2')
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
Array &0 (
-    0 => 'provider1'
+    0 => 'provider2'
     1 => 'first'
     2 => 'second'
)

/home/sb/DependencyAndDataProviderComboTest.php:31

FAILURES!
Tests: 4, Assertions: 4, Failures: 1.
Example 2.10 ひとつのテストでの複数のデータプロバイダの使用
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class DataTest extends TestCase
{
    /**
     * @dataProvider additionWithNonNegativeNumbersProvider
     * @dataProvider additionWithNegativeNumbersProvider
     */
    public function testAdd(int $a, int $b, int $expected): void
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionWithNonNegativeNumbersProvider(): array
    {
        return [
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }

    public function additionWithNegativeNumbersProvider(): array
    {
        return [
            [-1, 1, 0],
            [-1, -1, -2],
            [1, -1, 0]
        ];
    }
 }
$ phpunit DataTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

..F...                                                              6 / 6 (100%)

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) DataTest::testAdd with data set #3 (1, 1, 3)
Failed asserting that 2 is identical to 3.

/home/sb/DataTest.php:12

FAILURES!
Tests: 6, Assertions: 6, Failures: 1.

Note

あるテストがデータプロバイダを使う別のテストに依存している場合、 別のテストで少なくともひとつのデータセットに対するテストが成功すれば そのテストも実行されます。 データプロバイダを使ったテストの結果をそのテストに注入することはできません。

Note

すべてのデータプロバイダを実行してから、 静的メソッド setUpBeforeClass()setUp() メソッドの最初の呼び出しが発生します。そのため、 これらのメソッドで作った変数にデータプロバイダ内からアクセスすることはできません。 そうなっている理由は、PHPUnit がテストの総数を算出できるようにするためです。

例外のテスト

Example 2.11 は、テストするコード内で例外がスローされたかどうかを expectException() メソッドを使用して調べる方法を示すものです。

Example 2.11 expectException() メソッドの使用法
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class ExceptionTest extends TestCase
{
    public function testException(): void
    {
        $this->expectException(InvalidArgumentException::class);
    }
}
$ phpunit ExceptionTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Failed asserting that exception of type "InvalidArgumentException" is thrown.

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

expectException() メソッドのほかにも expectExceptionCode()expectExceptionMessage()expectExceptionMessageMatches() といったメソッドで、 テスト対象のコードで発生するであろう例外をテストできます。

Note

expectExceptionMessage()$actual の中に $expected のメッセージが含まれるかどうかを確かめるだけのものであり、 完全一致するかどうかを確かめるわけではないことに注意しましょう。

PHP のエラーのテスト

デフォルトでは、PHPUnit はテストの実行中に発生した PHP のエラーや警告そして notice を例外に変換します。これらの例外を用いて、たとえば Example 2.12 のように PHP のエラーが発生することをテストできます。

Note

PHP の実行時設定 error_reporting を使うと、 PHPUnit がどのエラーを例外に変換するのかを制限できます。 この機能に関して何か問題がでた場合は、PHP の設定を見直し、 調べたいと思っているエラーを抑制するようになっていないかどうか確認しましょう。

Example 2.12 PHPのエラーが発生することのテスト
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ErrorTest extends TestCase
{
    public function testDeprecationCanBeExpected(): void
    {
        $this->expectDeprecation();
        // Optionally test that the message is equal to a string
        $this->expectDeprecationMessage('foo');
        // Or optionally test that the message matches a regular expression
        $this->expectDeprecationMessageMatches('/foo/');
        \trigger_error('foo', \E_USER_DEPRECATED);
    }
    public function testNoticeCanBeExpected(): void
    {
        $this->expectNotice();
        // Optionally test that the message is equal to a string
        $this->expectNoticeMessage('foo');
        // Or optionally test that the message matches a regular expression
        $this->expectNoticeMessageMatches('/foo/');
        \trigger_error('foo', \E_USER_NOTICE);
    }
    public function testWarningCanBeExpected(): void
    {
        $this->expectWarning();
        // Optionally test that the message is equal to a string
        $this->expectWarningMessage('foo');
        // Or optionally test that the message matches a regular expression
        $this->expectWarningMessageMatches('/foo/');
        \trigger_error('foo', \E_USER_WARNING);
    }
    public function testErrorCanBeExpected(): void
    {
        $this->expectError();
        // Optionally test that the message is equal to a string
        $this->expectErrorMessage('foo');
        // Or optionally test that the message matches a regular expression
        $this->expectErrorMessageMatches('/foo/');
        \trigger_error('foo', \E_USER_ERROR);
    }
}

エラーを引き起こすような PHP の関数、たとえば fopen などに依存するテストを行うときには、テスト中にエラーを抑制できれば便利なことがあります。 そうすれば、notice のせいで PHPUnit\Framework\Error\Notice が出てしまうことなく、返り値だけをチェックできるようになります。

Example 2.13 PHP のエラーが発生するコードの返り値のテスト
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class ErrorSuppressionTest extends TestCase
{
    public function testFileWriting(): void
    {
        $writer = new FileWriter;

        $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
    }
}

final class FileWriter
{
    public function write($file, $content)
    {
        $file = fopen($file, 'w');

        if ($file === false) {
            return false;
        }

        // ...
    }
}
$ phpunit ErrorSuppressionTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

.

Time: 1 seconds, Memory: 5.25Mb

OK (1 test, 1 assertion)

もしエラーを抑制しなければ、このテストは失敗して fopen(/is-not-writeable/file): failed to open stream: No such file or directory となります。

出力内容のテスト

メソッドの実行結果を確かめる方法として、(echoprint などによる) 出力が期待通りのものかを調べたいこともあるでしょう。 PHPUnit\Framework\TestCase クラスは、PHP の 出力バッファリング 機能を使用してこの仕組みを提供します。

Example 2.14 では、期待する出力内容を expectOutputString() メソッドで設定する方法を示します。 期待通りの出力が得られなかった場合は、そのテストは失敗という扱いになります。

Example 2.14 関数やメソッドの出力内容のテスト
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class OutputTest extends TestCase
{
    public function testExpectFooActualFoo(): void
    {
        $this->expectOutputString('foo');

        print 'foo';
    }

    public function testExpectBarActualBaz(): void
    {
        $this->expectOutputString('bar');

        print 'baz';
    }
}
$ phpunit OutputTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) OutputTest::testExpectBarActualBaz
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

Table 2.1 は、 テストの出力用に提供するメソッドをまとめたものです。

Table 2.1 テストの出力用のメソッド
メソッド 意味
void expectOutputRegex(string $regularExpression) 出力が正規表現 $regularExpression にマッチするであろうという予測を設定します。
void expectOutputString(string $expectedString) 出力が文字列 $expectedString と等しくなるであろうという予測を設定します。
bool setOutputCallback(callable $callback) たとえば出力時の正規化などに使用するコールバック関数を設定します。
string getActualOutput() 実際の出力を取得します。

Note

strict モードでは、出力を発生させるテストは失敗します。

エラー出力

テストが失敗した場合、PHPUnit は、状況を可能な限り詳細に報告します。 これが、何が問題だったのかを調べるのに役立つでしょう。

Example 2.15 配列の比較に失敗したときのエラー出力
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class ArrayDiffTest extends TestCase
{
    public function testEquality(): void
    {
        $this->assertSame(
            [1, 2,  3, 4, 5, 6],
            [1, 2, 33, 4, 5, 6]
        );
    }
}
$ phpunit ArrayDiffTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayDiffTest::testEquality
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => 1
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )

/home/sb/ArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

この例では配列の要素のうちひとつだけが異なっています。 それ以外の値も表示することで、どこが悪かったのかをわかりやすくしています。

出力が長すぎる場合は PHPUnit が出力を分割し、違っている部分の前後数行だけを出力します。

Example 2.16 要素数の多い配列の比較に失敗したときのエラー出力
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class LongArrayDiffTest extends TestCase
{
    public function testEquality(): void
    {
        $this->assertSame(
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2,  3, 4, 5, 6],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 33, 4, 5, 6]
        );
    }
}
$ phpunit LongArrayDiffTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) LongArrayDiffTest::testEquality
Failed asserting that two arrays are identical.
--- Expected
+++ Actual
@@ @@
     13 => 2
-    14 => 3
+    14 => 33
     15 => 4
     16 => 5
     17 => 6
 )

/home/sb/LongArrayDiffTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

エッジケース

比較に失敗したときに、PHPUnit は入力値をテキスト形式にしてこれを比較します。 この実装が原因で、実際の違う箇所よりも多くの問題を報告してしまうことがあります。

この問題が発生するのは、 assertEquals() などの「緩い」比較の関数を、配列やオブジェクトに対して使った場合だけです。

Example 2.17 緩い比較を使った場合の diff の生成のエッジケース
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;

final class ArrayWeakComparisonTest extends TestCase
{
    public function testEquality(): void
    {
        $this->assertEquals(
            [1, 2, 3, 4, 5, 6],
            ['1', 2, 33, 4, 5, 6]
        );
    }
}
$ phpunit ArrayWeakComparisonTest
PHPUnit main.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) ArrayWeakComparisonTest::testEquality
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
-    0 => 1
+    0 => '1'
     1 => 2
-    2 => 3
+    2 => 33
     3 => 4
     4 => 5
     5 => 6
 )

/home/sb/ArrayWeakComparisonTest.php:7

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

この例では、最初のインデックスの 1'1' がエラー報告されていますが、 assertEquals() ではこれらを等しいとみなしているはずです。