第2章 PHPUnit 用のテストの書き方

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

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

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

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

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

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

例 2.1: PHPUnit での配列操作のテスト

<?php
use PHPUnit\Framework\TestCase;

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

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

        $this->assertEquals('foo', array_pop($stack));
        $this->assertEquals(0, count($stack));
    }
}
?>


 

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

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

 
 --Martin Fowler

テストの依存性

 

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 necessary an encapsulated, independent entity. Often there are implicit dependencies between test methods, hidden in the implementation scenario of a test.

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

 
 --Adrian Kuhn et. al.

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

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

  • コンシューマーとは、プロデューサーの返り値に依存するテストメソッドのこと。

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

例 2.2: @depends アノテーションを使った依存性の表現

<?php
use PHPUnit\Framework\TestCase;

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

        return $stack;
    }

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

        return $stack;
    }

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


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

注記

プロデューサーの生成する戻り値は、デフォルトでは「そのままの形式」でコンシューマーに渡されます。 つまり、プロデューサーがオブジェクトを戻した場合は、そのオブジェクトへの参照がコンシューマーに渡されるということです。 参照ではなくオブジェクトのコピーを渡す必要がある場合は、@depends ではなく @depends clone を使う必要があります。

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

例 2.3: テストの依存性の活用

<?php
use PHPUnit\Framework\TestCase;

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

    /**
     * @depends testOne
     */
    public function testTwo()
    {
    }
}
?>
phpunit --verbose DependencyFailureTest
PHPUnit 5.4.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 アノテーションを持つテストは、 最初のプロデューサーからのフィクスチャを最初の引数、二番目のプロデューサーからのフィクスチャを二番目の引数、…… として受け取ります。 例 2.4 を参照ください。

例 2.4: 複数の依存性を持つテスト

<?php
use PHPUnit\Framework\TestCase;

class MultipleDependenciesTest extends TestCase
{
    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     */
    public function testConsumer()
    {
        $this->assertEquals(
            ['first', 'second'],
            func_get_args()
        );
    }
}
?>
phpunit --verbose MultipleDependenciesTest
PHPUnit 5.4.0 by Sebastian Bergmann and contributors.

...

Time: 0 seconds, Memory: 3.25Mb

OK (3 tests, 3 assertions)


データプロバイダ

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

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

例 2.5: 配列の配列を返すデータプロバイダの使用

<?php
use PHPUnit\Framework\TestCase;

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

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3]
        ];
    }
}
?>
phpunit DataTest
PHPUnit 5.4.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 matches expected 3.

/home/sb/DataTest.php:9

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


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

例 2.6: データプロバイダでの名前つきデータセットの使用

<?php
use PHPUnit\Framework\TestCase;

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

    public function additionProvider()
    {
        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 matches expected 3.

/home/sb/DataTest.php:9

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


例 2.7: Iterator オブジェクトを返すデータプロバイダの使用

<?php
use PHPUnit\Framework\TestCase;

require 'CsvFileIterator.php';

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

    public function additionProvider()
    {
        return new CsvFileIterator('data.csv');
    }
}
?>
phpunit DataTest
PHPUnit 5.4.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 matches expected '3'.

/home/sb/DataTest.php:11

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


例 2.8: CsvFileIterator クラス

<?php
use PHPUnit\Framework\TestCase;

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

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

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

    public function rewind() {
        rewind($this->file);
        $this->current = fgetcsv($this->file);
        $this->key = 0;
    }

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

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

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

    public function next() {
        $this->current = fgetcsv($this->file);
        $this->key++;
    }
}
?>


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

例 2.9: 同じテストでの @depends と @dataProvider の組み合わせ

<?php
use PHPUnit\Framework\TestCase;

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

    public function testProducerFirst()
    {
        $this->assertTrue(true);
        return 'first';
    }

    public function testProducerSecond()
    {
        $this->assertTrue(true);
        return 'second';
    }

    /**
     * @depends testProducerFirst
     * @depends testProducerSecond
     * @dataProvider provider
     */
    public function testConsumer()
    {
        $this->assertEquals(
            ['provider1', 'first', 'second'],
            func_get_args()
        );
    }
}
?>
phpunit --verbose DependencyAndDataProviderComboTest
PHPUnit 5.4.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 equal.
--- Expected
+++ Actual
@@ @@
Array (
-    0 => 'provider1'
+    0 => 'provider2'
1 => 'first'
2 => 'second'
)

/home/sb/DependencyAndDataProviderComboTest.php:31

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


注記

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

注記

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

例外のテスト

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

例 2.10: expectException() メソッドの使用法

<?php
use PHPUnit\Framework\TestCase;

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

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

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


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

別の方法として、@expectedException@expectedExceptionCode@expectedExceptionMessage@expectedExceptionMessageRegExp といったアノテーションでも、 テスト対象のコードで発生するであろう例外をテストできます。 例 2.11 に例を示します。

例 2.11: @expectedException アノテーションの使用法

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    /**
     * @expectedException InvalidArgumentException
     */
    public function testException()
    {
    }
}
?>
phpunit ExceptionTest
PHPUnit 5.4.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ExceptionTest::testException
Expected exception InvalidArgumentException

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


PHP のエラーのテスト

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

注記

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

例 2.12: @expectedException を用いた、PHP エラーが発生することのテスト

<?php
use PHPUnit\Framework\TestCase;

class ExpectedErrorTest extends TestCase
{
    /**
     * @expectedException PHPUnit_Framework_Error
     */
    public function testFailingInclude()
    {
        include 'not_existing_file.php';
    }
}
?>
phpunit -d error_reporting=2 ExpectedErrorTest
PHPUnit 5.4.0 by Sebastian Bergmann and contributors.

.

Time: 0 seconds

OK (1 test, 1 assertion)


PHPUnit_Framework_Error_Notice および PHPUnit_Framework_Error_Warning は、 それぞれ PHP の notice と警告に対応します。

注記

例外をテストするときには可能な限り限定的にしなければいけません。 あまりに一般化されすぎたクラスをテストすると、予期せぬ副作用を引き起こしかねません。 というわけで、 @expectedExceptionsetExpectedException() を使った Exception クラスのテストはできないようにしました。

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

例 2.13: PHP のエラーが発生するコードの返り値のテスト

<?php
use PHPUnit\Framework\TestCase;

class ErrorSuppressionTest extends TestCase
{
    public function testFileWriting() {
        $writer = new FileWriter;
        $this->assertFalse(@$writer->write('/is-not-writeable/file', 'stuff'));
    }
}
class FileWriter
{
    public function write($file, $content) {
        $file = fopen($file, 'w');
        if($file == false) {
            return false;
        }
        // ...
    }
}

?>
phpunit ErrorSuppressionTest
PHPUnit 5.4.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 の 出力バッファリング 機能を使用してこの仕組みを提供します。

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

例 2.14: 関数やメソッドの出力内容のテスト

<?php
use PHPUnit\Framework\TestCase;

class OutputTest extends TestCase
{
    public function testExpectFooActualFoo()
    {
        $this->expectOutputString('foo');
        print 'foo';
    }

    public function testExpectBarActualBaz()
    {
        $this->expectOutputString('bar');
        print 'baz';
    }
}
?>
phpunit OutputTest
PHPUnit 5.4.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.


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

表2.1 テストの出力用のメソッド

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


注記

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

エラー出力

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

例 2.15: 配列の比較に失敗したときのエラー出力

<?php
use PHPUnit\Framework\TestCase;

class ArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2,  3, 4, 5, 6],
            [1, 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayDiffTest
PHPUnit 5.4.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 equal.
--- 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 が出力を分割し、違っている部分の前後数行だけを出力します。

例 2.16: 要素数の多い配列の比較に失敗したときのエラー出力

<?php
use PHPUnit\Framework\TestCase;

class LongArrayDiffTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [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 5.4.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 equal.
--- 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 などの「緩い」比較の関数を、配列やオブジェクトに対して使った場合だけです。

例 2.17: 緩い比較を使った場合の diff の生成のエッジケース

<?php
use PHPUnit\Framework\TestCase;

class ArrayWeakComparisonTest extends TestCase
{
    public function testEquality() {
        $this->assertEquals(
            [1, 2, 3, 4, 5, 6],
            ['1', 2, 33, 4, 5, 6]
        );
    }
}
?>
phpunit ArrayWeakComparisonTest
PHPUnit 5.4.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 ではこれらを等しいとみなしているはずです。

このページの改善案をGitHubで提案してください!