PHPUnit マニュアル

Bergmann Sebastian [FAMILY Given]

この作品は、Creative Commons Attribution License の下でライセンスされています。このライセンスの内容を確認するには、http://creativecommons.org/licenses/by/3.0/ を訪問するか、あるいは Creative Commons, 559 Nathan Abbott Way, Stanford, California 943.6, USA に手紙を送ってください。

PHPUnit 6.1 対応版 Updated on 2017-04-19.


1. PHPUnit のインストール
要件
PHP Archive (PHAR)
Windows
PHPUnit の PHAR リリースの検証
Composer
オプションのパッケージ
2. PHPUnit 用のテストの書き方
テストの依存性
データプロバイダ
例外のテスト
PHP のエラーのテスト
出力内容のテスト
エラー出力
エッジケース
3. コマンドラインのテストランナー
コマンドラインオプション
4. フィクスチャ
tearDown() よりも setUp()
バリエーション
フィクスチャの共有
グローバルな状態
5. テストの構成
ファイルシステムを用いたテストスイートの構成
XML 設定ファイルを用いたテストスイートの構成
6. リスクを伴うテスト
無意味なテスト
意図せぬうちにカバーされているコード
テストの実行時の出力
テストの実行時のタイムアウト
グローバルな状態の変更
7. 不完全なテスト・テストの省略
不完全なテスト
テストの省略
@requires によるテストのスキップ
8. データベースのテスト
データベースのテストに対応しているベンダー
データベースのテストの難しさ
データベーステストの四段階
1. データベースのクリーンアップ
2. フィクスチャの準備
3–5. テストの実行、結果の検証、そして後始末
PHPUnit のデータベーステストケースの設定
getConnection() の実装
getDataSet() の実装
データベーススキーマ (DDL) とは?
ヒント: 自前でのデータベーステストケースの抽象化
データセットとデータテーブルについて知る
利用できる実装
外部キーには注意
自作のデータセットやデータテーブルの実装
接続 API
データベースアサーション API
テーブルの行数のアサーション
テーブルの状態のアサーション
クエリの結果のアサーション
複数のテーブルの状態のアサーション
よくある質問
PHPUnit は、テストごとにデータベーススキーマを作り直すの?
PDO を使ったアプリケーションじゃないと Database Extension を使えないの?
Too much Connections というエラーが出たらどうすればいい?
フラット XML や CSV のデータセットで NULL を扱う方法は?
9. テストダブル
スタブ
モックオブジェクト
Prophecy
トレイトと抽象クラスのモック
ウェブサービスのスタブおよびモック
ファイルシステムのモック
10. テストの進め方
開発中のテスト
デバッグ中のテスト
11. コードカバレッジ解析
コードカバレッジの指標
ファイルのホワイトリスト
コードブロックの無視
カバーするメソッドの指定
エッジケース
12. テストのその他の使用法
アジャイルな文書作成
複数チームでのテスト
13. ログ出力
テスト結果 (XML)
テスト結果 (TAP)
テスト結果 (JSON)
コードカバレッジ (XML)
コードカバレッジ (テキスト)
14. PHPUnit の拡張
PHPUnit\Framework\TestCase のサブクラスの作成
カスタムアサーションの作成
PHPUnit_Framework_TestListener の実装
PHPUnit_Extensions_TestDecorator のサブクラスの作成
PHPUnit_Framework_Test の実装
A. アサーション
assertArrayHasKey()
assertClassHasAttribute()
assertArraySubset()
assertClassHasStaticAttribute()
assertContains()
assertContainsOnly()
assertContainsOnlyInstancesOf()
assertCount()
assertDirectoryExists()
assertDirectoryIsReadable()
assertDirectoryIsWritable()
assertEmpty()
assertEqualXMLStructure()
assertEquals()
assertFalse()
assertFileEquals()
assertFileExists()
assertFileIsReadable()
assertFileIsWritable()
assertGreaterThan()
assertGreaterThanOrEqual()
assertInfinite()
assertInstanceOf()
assertInternalType()
assertIsReadable()
assertIsWritable()
assertJsonFileEqualsJsonFile()
assertJsonStringEqualsJsonFile()
assertJsonStringEqualsJsonString()
assertLessThan()
assertLessThanOrEqual()
assertNan()
assertNull()
assertObjectHasAttribute()
assertRegExp()
assertStringMatchesFormat()
assertStringMatchesFormatFile()
assertSame()
assertStringEndsWith()
assertStringEqualsFile()
assertStringStartsWith()
assertThat()
assertTrue()
assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString()
B. アノテーション
@author
@after
@afterClass
@backupGlobals
@backupStaticAttributes
@before
@beforeClass
@codeCoverageIgnore*
@covers
@coversDefaultClass
@coversNothing
@dataProvider
@depends
@expectedException
@expectedExceptionCode
@expectedExceptionMessage
@expectedExceptionMessageRegExp
@group
@large
@medium
@preserveGlobalState
@requires
@runTestsInSeparateProcesses
@runInSeparateProcess
@small
@test
@testdox
@ticket
@uses
C. XML 設定ファイル
PHPUnit
テストスイート
グループ
コードカバレッジ対象のファイルのホワイトリスト
ログ出力
テストリスナー
PHP INI 項目や定数、グローバル変数の設定
D. 目次
E. 参考文献
F. 著作権

第1章 PHPUnit のインストール

要件

PHPUnit 6.1 は PHP 5.6 以降のバージョンで動作しますが、最新版の PHP を使うことを強く推奨します。

PHPUnit を使うには、拡張モジュール domjson、 が必要です。これらは通常、デフォルトで有効になっています。

PHPUnit また、拡張モジュール pcrereflection、 そして spl も必要です。これらは標準の拡張モジュールとしてデフォルトで有効になっており、 PHP のビルドシステムやソースファイルに手を加えない限り、 無効にすることはできません。

コードカバレッジをサポートするには Xdebug 2.2.1 以降と tokenizer 拡張モジュールが必要です。 XML 形式で情報を出力するには、xmlwriter 拡張モジュールも必要です。

PHP Archive (PHAR)

PHPUnit を入手する一番簡単な方法は、PHP Archive (PHAR) をダウンロードすることです。 必要な依存コンポーネントがすべて (オプションのコンポーネントの一部も含めて) ひとつのファイルにまとめられています。

PHP Archives (PHAR) を利用するには、 phar 拡張モジュールが必要です。

PHAR の --self-update 機能を使うには、 openssl 拡張モジュールが必要です。

Suhosin 拡張モジュールが有効になっている場合は、 php.ini で PHAR の実行を許可する必要があります。

suhosin.executor.include.whitelist = phar

PHAR をグローバルにインストールするには、次のようにします。

$ wget https://phar.phpunit.de/phpunit.phar
$ chmod +x phpunit.phar
$ sudo mv phpunit.phar /usr/local/bin/phpunit
$ phpunit --version
PHPUnit x.y.z by Sebastian Bergmann and contributors.

ダウンロードした PHAR ファイルを直接使ってもかまいません。

$ wget https://phar.phpunit.de/phpunit.phar
$ php phpunit.phar --version
PHPUnit x.y.z by Sebastian Bergmann and contributors.
wget https://phar.phpunit.de/phpunit.phar
php phpunit.phar

Windows

PHAR をグローバルにインストールする方法は、 Composer を Windows に手動でインストールする のと同じ手順です。

  1. PHP バイナリ用のディレクトリを作ります(例:C:\bin

  2. ;C:\bin を、環境変数 PATH に追記します (参考資料)。

  3. https://phar.phpunit.de/phpunit.phar をダウンロードして、 C:\bin\phpunit.phar に保存します。

  4. コマンドプロンプトを開きます ( Windows+R » cmd » ENTER)。

  5. 以下のようにして、バッチスクリプト (C:\bin\phpunit.cmd) を作ります。

    C:\Users\username> cd C:\bin
    C:\bin> echo @php "%~dp0phpunit.phar" %* > phpunit.cmd
    C:\bin> exit
    
  6. コマンドプロンプトをもう一枚開き、どこからでも PHPUnit を実行できることを確認します。

    C:\Users\username> phpunit --version
    PHPUnit x.y.z by Sebastian Bergmann and contributors.
    

Cygwin や MingW32 (TortoiseGit など) のシェル環境で使う場合は、 五番目のステップは飛ばしてもかまいません。単にファイルを phpunit という名前 (拡張子 .phar は不要) で保存して、あとは chmod 775 phpunit で実行可能にしておきましょう。

PHPUnit の PHAR リリースの検証

PHPUnit プロジェクトが配布する公式リリースにはすべて、 リリースマネージャーによる署名がついています。 検証用の PGP 署名と SHA1 ハッシュは、phar.phpunit.de から取得できます。

リリースの検証をどのように行うのかについて、説明しましょう。まず、 phpunit.phar をダウンロードし、さらにその PGP 署名 phpunit.phar.asc もダウンロードします。

wget https://phar.phpunit.de/phpunit.phar
wget https://phar.phpunit.de/phpunit.phar.asc

ダウンロードした PHPUnit の PHP Archive (phpunit.phar) を、署名 (phpunit.phar.asc) で検証します。

gpg phpunit.phar.asc
gpg: Signature made Sat 19 Jul 2014 01:28:02 PM CEST using RSA key ID 6372C20A
gpg: Can't check signature: public key not found

リリースマネージャーの公開鍵 (6372C20A) が、ローカルシステム上に存在しないようです。 検証を進めるには、リリースマネージャーの公開鍵を、鍵サーバーから取得する必要があります。 鍵サーバーには、たとえば pgp.uni-mainz.de などがあります。 公開鍵サーバーはお互いリンクしあっているので、どの鍵サーバーを使ってもかまいません。

gpg --keyserver pgp.uni-mainz.de --recv-keys 0x4AA394086372C20A
gpg: requesting key 6372C20A from hkp server pgp.uni-mainz.de
gpg: key 6372C20A: public key "Sebastian Bergmann <sb@sebastian-bergmann.de>" imported
gpg: Total number processed: 1
gpg:               imported: 1  (RSA: 1)

これで、"Sebastian Bergmann <sb@sebastian-bergmann.de>" さんの公開鍵を取得できました。 ただ、この鍵を作ったのが本当に Sebastian Bergmann という人なのかは、確かめようがありません。 ともあれ、もう一度リリースの署名を検証してみましょう。

gpg phpunit.phar.asc
gpg: Signature made Sat 19 Jul 2014 01:28:02 PM CEST using RSA key ID 6372C20A
gpg: Good signature from "Sebastian Bergmann <sb@sebastian-bergmann.de>"
gpg:                 aka "Sebastian Bergmann <sebastian@php.net>"
gpg:                 aka "Sebastian Bergmann <sebastian@thephp.cc>"
gpg:                 aka "Sebastian Bergmann <sebastian@phpunit.de>"
gpg:                 aka "Sebastian Bergmann <sebastian.bergmann@thephp.cc>"
gpg:                 aka "[jpeg image of size 40635]"
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: D840 6D0D 8294 7747 2937  7831 4AA3 9408 6372 C20A

とりあえず、署名が正しいことはわかりました。 ただ、この署名が信頼できるものであるかどうかは、まだわかりません。 ここで言う「署名が正しい」とは、リリースのファイルが改ざんされていないということです。 しかし、公開鍵暗号方式の性質上、これだけでは不十分です。 6372C20A を作ったのが Sebastian Bergmann 本人であることを、確かめる必要があります。

公開鍵を作って公開鍵サーバーにアップロードするのは、誰にだってできることです。 当然、悪意のある攻撃者にも可能なことです。 攻撃者は、このニセの鍵を使って署名した、悪意のあるリリースを作ることもできます。 このリリース (そして署名) をダウンロードして検証すると、成功するでしょう。 なぜならその公開鍵は、悪意のある攻撃者が作ったニセの鍵だからです。 こういったことを防ぐために、鍵の作者も検証しなければいけないのです。 公開鍵の作者を検証する方法については、このマニュアルの範囲を超えるので、割愛します。

PHPUnit のインストールを管理するためのシェルスクリプトを用意しておくのもいいでしょう。 GnuPG の署名を検証してから、テストスイートを実行させるようなものです。たとえば次のようになります。

#!/usr/bin/env bash
clean=1 # Delete phpunit.phar after the tests are complete?
aftercmd="php phpunit.phar --bootstrap bootstrap.php src/tests"
gpg --fingerprint D8406D0D82947747293778314AA394086372C20A
if [ $? -ne 0 ]; then
    echo -e "\033[33mDownloading PGP Public Key...\033[0m"
    gpg --recv-keys D8406D0D82947747293778314AA394086372C20A
    # Sebastian Bergmann <sb@sebastian-bergmann.de>
    gpg --fingerprint D8406D0D82947747293778314AA394086372C20A
    if [ $? -ne 0 ]; then
        echo -e "\033[31mCould not download PGP public key for verification\033[0m"
        exit
    fi
fi

if [ "$clean" -eq 1 ]; then
    # Let's clean them up, if they exist
    if [ -f phpunit.phar ]; then
        rm -f phpunit.phar
    fi
    if [ -f phpunit.phar.asc ]; then
        rm -f phpunit.phar.asc
    fi
fi

# 最新のリリースとその署名の取得
if [ ! -f phpunit.phar ]; then
    wget https://phar.phpunit.de/phpunit.phar
fi
if [ ! -f phpunit.phar.asc ]; then
    wget https://phar.phpunit.de/phpunit.phar.asc
fi

# 実行前の検証
gpg --verify phpunit.phar.asc phpunit.phar
if [ $? -eq 0 ]; then
    echo
    echo -e "\033[33mBegin Unit Testing\033[0m"
    # Run the testing suite
    `$after_cmd`
    # Cleanup
    if [ "$clean" -eq 1 ]; then
        echo -e "\033[32mCleaning Up!\033[0m"
        rm -f phpunit.phar
        rm -f phpunit.phar.asc
    fi
else
    echo
    chmod -x phpunit.phar
    mv phpunit.phar /tmp/bad-phpunit.phar
    mv phpunit.phar.asc /tmp/bad-phpunit.phar.asc
    echo -e "\033[31mSignature did not match! PHPUnit has been moved to /tmp/bad-phpunit.phar\033[0m"
    exit 1
fi
      

Composer

Composer を使ってプロジェクトの依存関係を管理するには、 phpunit/phpunit への依存情報をプロジェクトの composer.json ファイルに追加します。 次に示すのは最小限の composer.json ファイルの例で、 開発時の PHPUnit 6.1 への依存を定義しています。

{
    "require-dev": {
        "phpunit/phpunit": "5.5.*"
    }
}

システム全体で使えるように Composer でインストールするには、次のようにします。

composer global require "phpunit/phpunit=5.5.*"

~/.composer/vendor/bin/ にパスを通すことを忘れないようにしましょう。

オプションのパッケージ

オプションのパッケージとして、これらが使えます。

PHP_Invoker

callable をタイムアウトつきで実行するユーティリティクラス。 テストのタイムアウトを厳格に指定するために必要なパッケージ。

このパッケージは、PHPUnit の PHAR 版の中に含まれています。 Composer でインストールするには、 "require-dev" に次の行を追加します。

"phpunit/php-invoker": "*"
DbUnit

DbUnit の PHP/PHPUnit 向けの移植。データベースとのやりとりをテスト可能にする。

このパッケージは、PHPUnit の PHAR 版の中に含まれていません。 Composer でインストールするには、 "require-dev" に次の行を追加します。

"phpunit/dbunit": ">=1.2"

第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 6.1.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 6.1.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 6.1.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 6.1.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 6.1.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 6.1.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 6.1.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 6.1.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 6.1.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 6.1.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)たとえば出力時の正規化などに使用するコールバック関数を設定します。
string getActualOutput()実際の出力を取得します。

注記

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 6.1.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 6.1.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 6.1.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 ではこれらを等しいとみなしているはずです。

第3章 コマンドラインのテストランナー

phpunit コマンドを実行すると、PHPUnit のコマンドライン版テストランナーが起動します。 コマンドラインのテストランナーを使用したテストの様子を以下に示します。

phpunit ArrayTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

..

Time: 0 seconds


OK (2 tests, 2 assertions)

このように実行すると、PHPUnit のコマンドラインテストランナーは、 まず現在の作業ディレクトリにあるソースファイル ArrayTest.php を探してそれを読み込み、テストケースクラス ArrayTest を探します。 そして、そのクラス内のテストを実行します。

テストがひとつ実行されるたびに、PHPUnit コマンドラインツールはその経過を示す文字を出力します。

.

テストが成功した際に表示されます。

F

テストメソッドの実行中、アサーションに失敗した際に表示されます。

E

テストメソッドの実行中、エラーが発生した際に表示されます。

R

テストが危険だとマークされている場合に表示されます (??? を参照ください)。

S

テストが飛ばされた場合に表示されます (第 7 章 を参照ください)。

I

テストが「不完全」あるいは「未実装」とマークされている場合に表示されます (第 7 章 を参照ください)。

PHPUnit は、失敗 (failures)エラー (errors) を区別します。 「失敗」は PHPUnit のアサーションに違反した場合、つまり例えば assertEquals() のコールに失敗した場合などで、 「エラー」は予期せぬ例外や PHP のエラーが発生した場合となります。 この区別は、時に有用です。というのは「エラー」は一般的に「失敗」 より修正しやすい傾向があるからです。 もし大量の問題が発生した場合は、まず「エラー」を最初に片付け、 その後で「失敗」を修正していくのが最良の方法です。

コマンドラインオプション

以下のコードで、コマンドライン版テストランナーのオプションの一覧を見てみましょう。

phpunit --help
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

Usage: phpunit [options] UnitTest [UnitTest.php]
       phpunit [options] <directory>

Code Coverage Options:

  --coverage-clover <file>  Generate code coverage report in Clover XML format.
  --coverage-crap4j <file>  Generate code coverage report in Crap4J XML format.
  --coverage-html <dir>     Generate code coverage report in HTML format.
  --coverage-php <file>     Export PHP_CodeCoverage object to file.
  --coverage-text=<file>    Generate code coverage report in text format.
                            Default: Standard output.
  --coverage-xml <dir>      Generate code coverage report in PHPUnit XML format.

Logging Options:

  --log-junit <file>        Log test execution in JUnit XML format to file.
  --log-tap <file>          Log test execution in TAP format to file.
  --log-json <file>         Log test execution in JSON format.
  --testdox-html <file>     Write agile documentation in HTML format to file.
  --testdox-text <file>     Write agile documentation in Text format to file.

Test Selection Options:

  --filter <pattern>        Filter which tests to run.
  --testsuite <pattern>     Filter which testsuite to run.
  --group ...               Only runs tests from the specified group(s).
  --exclude-group ...       Exclude tests from the specified group(s).
  --list-groups             List available test groups.
  --test-suffix ...         Only search for test in files with specified
                            suffix(es). Default: Test.php,.phpt

Test Execution Options:

  --report-useless-tests    Be strict about tests that do not test anything.
  --strict-coverage         Be strict about unintentionally covered code.
  --strict-global-state     Be strict about changes to global state
  --disallow-test-output    Be strict about output during tests.
  --enforce-time-limit      Enforce time limit based on test size.
  --disallow-todo-tests     Disallow @todo-annotated tests.

  --process-isolation       Run each test in a separate PHP process.
  --no-globals-backup       Do not backup and restore $GLOBALS for each test.
  --static-backup           Backup and restore static attributes for each test.

  --colors=<flag>           Use colors in output ("never", "auto" or "always").
  --columns <n>             Number of columns to use for progress output.
  --columns max             Use maximum number of columns for progress output.
  --stderr                  Write to STDERR instead of STDOUT.
  --stop-on-error           Stop execution upon first error.
  --stop-on-failure         Stop execution upon first error or failure.
  --stop-on-risky           Stop execution upon first risky test.
  --stop-on-skipped         Stop execution upon first skipped test.
  --stop-on-incomplete      Stop execution upon first incomplete test.
  -v|--verbose              Output more verbose information.
  --debug                   Display debugging information during test execution.

  --loader <loader>         TestSuiteLoader implementation to use.
  --repeat <times>          Runs the test(s) repeatedly.
  --tap                     Report test execution progress in TAP format.
  --testdox                 Report test execution progress in TestDox format.
  --printer <printer>       TestListener implementation to use.

Configuration Options:

  --bootstrap <file>        A "bootstrap" PHP file that is run before the tests.
  -c|--configuration <file> Read configuration from XML file.
  --no-configuration        Ignore default configuration file (phpunit.xml).
  --include-path <path(s)>  Prepend PHP's include_path with given path(s).
  -d key[=value]            Sets a php.ini value.

Miscellaneous Options:

  -h|--help                 Prints this usage information.
  --version                 Prints the version and exits.
phpunit UnitTest

UnitTest という名前のクラスで定義されている テストを実行します。このクラスは、UnitTest.php という名前のファイルの中に定義されているものとします。

UnitTest は、PHPUnit\Framework\TestCase を継承したクラスであるか、あるいは PHPUnit_Framework_Test オブジェクト、例えば PHPUnit_Framework_TestSuite のインスタンスを返す public static suite() というメソッドを保持するクラスでなければなりません。

phpunit UnitTest UnitTest.php

UnitTest という名前のクラスで定義されているテストを実行します。 このクラスは、指定したファイルの中で定義されているものとします。

--coverage-clover

テスト結果から XML 形式のログファイルを作成し、 コードカバレッジ情報もそこに含めます。 詳細は 第 13 章 を参照ください。

この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。

--coverage-crap4j

コードカバレッジレポートを Crap4j 形式で作成します。詳細は 第 11 章 を参照ください。

この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。

--coverage-html

コードカバレッジレポートを HTML 形式で作成します。詳細は 第 11 章 を参照ください。

この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。

--coverage-php

シリアライズした PHP_CodeCoverage オブジェクトを生成し、 コードカバレッジ情報もそこに含めます。

この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。

--coverage-text

テストを実行したときに、ログファイルあるいはコマンドライン出力で 可読形式のコードカバレッジ情報を生成します。 詳細は 第 13 章 を参照ください。

この機能は、tokenizer 拡張モジュールおよび Xdebug 拡張モジュールがインストールされている場合にのみ使用可能となることに注意しましょう。

--log-junit

JUnit XML フォーマットを使用して、テストの実行結果のログを作成します。 詳細は 第 13 章 を参照ください。

--log-tap

Test Anything Protocol (TAP) フォーマットを使用して、テストの実行結果のログを作成します。 詳細は 第 13 章 を参照ください。

--log-json

JSON フォーマットを使用して、ログファイルを作成します。 詳細は 第 13 章 を参照ください。

--testdox-html および --testdox-text

実行したテストについて、HTML あるいはプレーンテキスト形式のドキュメントを生成します 詳細は 第 12 章 を参照ください。

--filter

指定した正規表現パターンにマッチする名前のテストのみを実行します。 パターンがデリミタで囲まれていない場合は、PHPUnit はパターンをデリミタ / で囲みます。

マッチするテスト名は、次のいずれかのフォーマットになります。

TestNamespace\TestCaseClass::testMethod

デフォルトのテスト名のフォーマットは、テストメソッドの中でマジック定数 __METHOD__ を使うのと同等です。

TestNamespace\TestCaseClass::testMethod with data set #0

テストがデータプロバイダーを持つ場合、データを処理するたびに、 現在のインデックスをデフォルトのテスト名の後に続けたものを取得します。

TestNamespace\TestCaseClass::testMethod with data set "my named data"

テストが持つデータプロバイダーが名前つきセットを使う場合、データを処理するたびに、 現在の名前をデフォルトのテスト名の後に続けたものを取得します。 名前つきデータセットの例は 例 3.1 を参照ください。

例 3.1: 名前つきデータセット

<?php
use PHPUnit\Framework\TestCase;

namespace TestNamespace;

class TestCaseClass extends TestCase
{
    /**
     * @dataProvider provider
     */
    public function testMethod($data)
    {
        $this->assertTrue($data);
    }

    public function provider()
    {
        return [
            'my named data' => [true],
            'my data'       => [true]
        ];
    }
}
?>

/path/to/my/test.phpt

PHPT のテストのテスト名は、ファイルシステムのパスになります。

有効なフィルターパターンの例は、例 3.2 を参照ください。

例 3.2: フィルターパターンの例

  • --filter 'TestNamespace\\TestCaseClass::testMethod'

  • --filter 'TestNamespace\\TestCaseClass'

  • --filter TestNamespace

  • --filter TestCaseClass

  • --filter testMethod

  • --filter '/::testMethod .*"my named data"/'

  • --filter '/::testMethod .*#5$/'

  • --filter '/::testMethod .*#(5|6|7)$/'


データプロバイダーのマッチングに使えるショートカットは、 例 3.3 を参照ください。

例 3.3: フィルターのショートカット

  • --filter 'testMethod#2'

  • --filter 'testMethod#2-4'

  • --filter '#2'

  • --filter '#2-4'

  • --filter 'testMethod@my named data'

  • --filter 'testMethod@my.*data'

  • --filter '@my named data'

  • --filter '@my.*data'


--testsuite

指定したパターンにマッチする名前のテストスイートのみを実行します。

--group

指定したグループのテストのみを実行します。 あるテストを特定のグループに所属させるには、 @group アノテーションを使用します。

@author アノテーションは @group のエイリアスで、 テストの作者に基づいてテストをフィルタリングします。

--exclude-group

指定したグループをテストの対象外とします。 あるテストを特定のグループに所属させるには、 @group アノテーションを使用します。

--list-groups

使用可能なテストグループの一覧を表示します。

--test-suffix

指定したサフィックスのテストファイルだけを探します。

--report-useless-tests

何もテストをしないテストについて厳格にチェックします。 詳細は ??? を参照ください。

--strict-coverage

意図せずカバーしているコードについて厳格にチェックします。 詳細は ??? を参照ください。

--strict-global-state

グローバルな状態の変更について厳格にチェックします。 詳細は ??? を参照ください。

--disallow-test-output

実行中に何かを出力するテストについて厳格にチェックします。 詳細は ??? を参照ください。

--disallow-todo-tests

docblock に @todo アノテーションが指定されているテストを実行しません。

--enforce-time-limit

テストのサイズに応じて、制限時間を設定します。 詳細は ??? を参照ください。

--process-isolation

各テストを個別の PHP プロセスで実行します。

--no-globals-backup

$GLOBALS のバックアップ・リストアを行いません。 詳細は 「グローバルな状態」 を参照ください。

--static-backup

ユーザ定義クラスの静的属性のバックアップ・リストアを行います。 詳細は 「グローバルな状態」 を参照ください。

--colors

出力に色を使用します。 Windows では、ANSICON あるいは ConEmu を利用します。

指定できる値は、以下の三つです。

  • never: 出力を色分けしません。これは、--colors オプションを省略したときのデフォルトです。

  • auto: ターミナルが色をサポートしていない場合や、 出力をコマンドにパイプしたりファイルにリダイレクトしたりする場合を除き、出力に色を使います。

  • always: ターミナルが色をサポートしていない場合や、 出力をコマンドにパイプしたりファイルにリダイレクトしたりする場合も含めて、常に出力に色を使います。

--colors だけを指定して何も値を指定しなかった場合は、auto を選んだものとみなされます。

--stderr

オプションで、出力先を STDOUT ではなく STDERR にします。

--stop-on-error

最初にエラーが発生した時点で実行を停止します。

--stop-on-failure

最初にエラーあるいは失敗が発生した時点で実行を停止します。

--stop-on-risky

最初に危険なテストがあらわれた時点で実行を停止します。

--stop-on-skipped

最初にテストのスキップが発生した時点で実行を停止します。

--stop-on-incomplete

最初に不完全なテストがあらわれた時点で実行を停止します。

--verbose

より詳細な情報を出力します。例えば、 未完成のテストや省略したテストの名前が表示されます。

--debug

テスト名などのデバッグ情報を、テストの実行開始時に出力します。

--loader

PHPUnit_Runner_TestSuiteLoader を実装したクラスのうち、 実際に使用するものを指定します。

標準のテストスイートローダーは、現在の作業ディレクトリおよび PHP の設定項目 include_path で指定されているディレクトリからソースファイルを探します。 Project_Package_Class クラスがソースファイル Project/Package/Class.php に対応します。

--repeat

指定された回数だけ、繰り返しテストを実行します。

--tap

Test Anything Protocol (TAP) を使用して、テストの進行状況を報告します。 詳細は 第 13 章 を参照ください。

--testdox

テストの進行状況を、アジャイルな文書として報告します。 詳細は 第 12 章 を参照ください。

--printer

結果を表示するために使うプリンタクラスを指定します。このプリンタクラスは PHPUnit_Util_Printer を継承し、かつ PHPUnit_Framework_TestListener インターフェイスを実装したものでなければなりません。

--bootstrap

テストの前に実行される "ブートストラップ" PHP ファイルを指定します。

--configuration, -c

設定を XML ファイルから読み込みます。 詳細は 付録 C を参照ください。

phpunit.xml あるいは phpunit.xml.dist (この順番で使用します) が現在の作業ディレクトリに存在しており、かつ --configuration が使われていない場合、設定が自動的にそのファイルから読み込まれます。

--no-configuration

現在の作業ディレクトリにある phpunit.xml および phpunit.xml.dist を無視します。

--include-path

PHP の include_path の先頭に、指定したパスを追加します。

-d

指定した PHP 設定オプションの値を設定します。

注記

PHPUnit 4.8 以降では、これらのオプションを引数の後にも指定できるようになりました。

第4章 フィクスチャ

テストを記述する際にいちばん時間を食うのは、テストを開始するための事前設定と テスト終了後の後始末の処理を書くことです。この事前設定は、テストの フィクスチャ と呼ばれます。

例 2.1 では、フィクスチャは $stack という変数に格納された配列だけでした。 しかし、たいていの場合はフィクスチャはこれより複雑なものとなり、 それを準備するにはかなりの量のコードが必要です。本来のテストの内容が、 フィクスチャを設定するためのコードの中に埋もれてしまうことになります。 この問題は、複数のテストで同じようなフィクスチャを設定する場合により顕著になります。 テストフレームワークの助けがなければ、 個々のテストのなかで同じような準備コードを繰り返し書くはめになってしまいます。

PHPUnit は、準備用のコードの共有をサポートしています。 各テストメソッドが実行される前に、setUp() という名前のテンプレートメソッドが実行されます。setUp() は、テスト対象のオブジェクトを生成するような処理に使用します。 テストメソッドの実行が終了すると、それが成功したか否かにかかわらず、 tearDown() という名前の別のテンプレートメソッドが実行されます。 tearDown() では、テスト対象のオブジェクトの後始末などを行います。

例 2.2 producer-consumer の関係を使って複数のテストでフィクスチャを共有しました。 これは常に要求されるというものではありませんし、常に可能だとも限りません。 例 4.1 では、 フィクスチャを再利用するのではなくコードで作成する方式で StackTest にテストを書く方法をごらんいただきましょう。 まずインスタンス変数 $stack を宣言し、 メソッドローカル変数のかわりにこれを使うことにします。 そして、array の作成を setUp() メソッドで行います。 最後に、冗長なコードをテストメソッドから削除し、 アサーションメソッド assertEquals() ではメソッド変数 $stack のかわりに 新たに導入したインスタンス変数 $this->stack を使うようにします。

例 4.1: setUp() を使用して stack フィクスチャを作成する

<?php
use PHPUnit\Framework\TestCase;

class StackTest extends TestCase
{
    protected $stack;

    protected function setUp()
    {
        $this->stack = [];
    }

    public function testEmpty()
    {
        $this->assertTrue(empty($this->stack));
    }

    public function testPush()
    {
        array_push($this->stack, 'foo');
        $this->assertEquals('foo', $this->stack[count($this->stack)-1]);
        $this->assertFalse(empty($this->stack));
    }

    public function testPop()
    {
        array_push($this->stack, 'foo');
        $this->assertEquals('foo', array_pop($this->stack));
        $this->assertTrue(empty($this->stack));
    }
}
?>

テンプレートメソッド setUp() および tearDown() は、テストケースクラスのテストメソッドごとに (そして最初にインスタンスを作成したときに) 一度ずつ実行されます。

さらに、テンプレートメソッド setUpBeforeClass() および tearDownAfterClass() が存在します。 これらはそれぞれ、テストケースクラスの最初のテストメソッドの実行前と テストケースクラスの最後のテストの実行後にコールされます。

以下の例は、テストケースクラスで使用できるすべてのテンプレートメソッドを示すものです。

例 4.2: 利用可能なすべてのテンプレートメソッド

<?php
use PHPUnit\Framework\TestCase;

class TemplateMethodsTest extends TestCase
{
    public static function setUpBeforeClass()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function setUp()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function assertPreConditions()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    public function testOne()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        $this->assertTrue(true);
    }

    public function testTwo()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        $this->assertTrue(false);
    }

    protected function assertPostConditions()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function tearDown()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    public static function tearDownAfterClass()
    {
        fwrite(STDOUT, __METHOD__ . "\n");
    }

    protected function onNotSuccessfulTest(Exception $e)
    {
        fwrite(STDOUT, __METHOD__ . "\n");
        throw $e;
    }
}
?>
phpunit TemplateMethodsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

TemplateMethodsTest::setUpBeforeClass
TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testOne
TemplateMethodsTest::assertPostConditions
TemplateMethodsTest::tearDown
.TemplateMethodsTest::setUp
TemplateMethodsTest::assertPreConditions
TemplateMethodsTest::testTwo
TemplateMethodsTest::tearDown
TemplateMethodsTest::onNotSuccessfulTest
FTemplateMethodsTest::tearDownAfterClass


Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) TemplateMethodsTest::testTwo
Failed asserting that <boolean:false> is true.
/home/sb/TemplateMethodsTest.php:30

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

tearDown() よりも setUp()

setUp()tearDown() は理屈上では対称的になるはずですが、実際にはそうではありません。実際には、 tearDown() を実装する必要があるのは setUp() で外部リソース (ファイルやソケットなど) を割り当てた場合のみです。もし setUp() で単に PHP オブジェクトを作成しただけの場合は、 一般には tearDown() は必要ありません。しかし、もし setUp() で大量のオブジェクトを作成した場合には、 それらの後始末をするために tearDown() で変数を unset() したくなることもあるでしょう。 テストケースオブジェクト自体のガベージコレクションにはあまり意味がありません。

バリエーション

ふたつのテストがあって、それぞれの setup がほんの少しだけ違う場合にはどうなるでしょう? このような場合は、二種類の可能性が考えられます。

  • もし setUp() の違いがごくわずかなものなら、 その違う部分を setUp() からテストメソッドのほうに移動させます。

  • setUp() の違いが大きければ、 テストケースクラスを別に分ける必要があります。それぞれのクラスには、 setup の違いを表す名前をつけます。

フィクスチャの共有

複数のテストの間でフィクスチャを共有する利点は、ほとんどありません。 しかし、設計上の問題などでどうしても フィクスチャを共有しなければならないこともあるでしょう。

複数のテスト間で共有する意味のあるフィクスチャの例として意味のあるものといえば、 データベースとの接続でしょう。テストのたびに新しいデータベース接続を毎回作成するのではなく、 最初にログインした状態を再利用するということです。こうすることで、 テストの実行時間を短縮できます。

例 4.3 では、テンプレートメソッド setUpBeforeClass() および tearDownAfterClass() を用いて、 テストケースクラス内の最初のテストを実行する前にデータベースに接続し、 最後のテストが終わってから接続を切断するようにしています。

例 4.3: テストスイートの複数テスト間でのフィクスチャの共有

<?php
use PHPUnit\Framework\TestCase;

class DatabaseTest extends TestCase
{
    protected static $dbh;

    public static function setUpBeforeClass()
    {
        self::$dbh = new PDO('sqlite::memory:');
    }

    public static function tearDownAfterClass()
    {
        self::$dbh = null;
    }
}
?>

このようにフィクスチャを共有することがテストの価値を下げてしまうということを、 まだうまく伝え切れていないかもしれません。問題なのは、 各オブジェクトが疎結合になっていないという設計なのです。 複数が連携しているようなテストを作って設計上の問題から目をそらしてしまうのではなく、 きちんと設計しなおした上で、スタブ (第 9 章 を参照ください) を使用するテストを書くことをお勧めします。

グローバルな状態

singleton を使用するコードをテストするのはたいへんです。 同様に、グローバル変数を使うコードのテストもまたたいへんです。 一般に、テスト対象のコードはグローバル変数と密接に関連しており、 グローバル変数の内容を制御することはできません。 さらに別の問題もあって、あるテストの中でグローバル変数を変更してしまうと 別のテストがうまく動かなくなる可能性があります。

PHP では、グローバル変数は次のような動きをします。

  • グローバル変数 $foo = 'bar'; は、$GLOBALS['foo'] = 'bar'; として格納される。

  • $GLOBALSスーパーグローバル変数と呼ばれる。

  • スーパーグローバル変数は組み込みの変数で、すべてのスコープで常に利用できる。

  • 関数やメソッドのスコープでグローバル変数 $foo にアクセスするには、直接 $GLOBALS['foo'] にアクセスするか、あるいは global $foo; を用いて (グローバル変数を参照する) ローカル変数を作成する。

グローバル変数のほかに、クラスの静的属性もグローバル状態となります。

デフォルトでは、PHPUnit がテストを実行する際には、 グローバル変数やスーパーグローバル変数 ($GLOBALS, $_ENV, $_POST, $_GET, $_COOKIE, $_SERVER, $_FILES, $_REQUEST) への変更が他のテストへの影響を及ぼさないようにします。 オプションで、この分離をクラスの静的属性まで拡張することもできます。

注記

グローバル変数やクラスの静的属性のバックアップ・リストアには serialize() および unserialize() を使用しています。

PHP 組み込みの一部のクラス、たとえば PDO のオブジェクトはシリアライズできないため、そのようなオブジェクトが $GLOBALS 配列に格納されている場合はバックアップ操作が失敗します。

「@backupGlobals」 で説明している @backupGlobals アノテーションを使用すると、 グローバル変数のバックアップ・リストア操作を制御することができます。 あるいは、グローバル変数のブラックリストを指定して、 その変数だけはバックアップ・リストアの対象から除外することもできます。

class MyTest extends TestCase
{
    protected $backupGlobalsBlacklist = ['globalVariable'];

    // ...
}

注記

$backupGlobalsBlacklist プロパティをたとえば setUp() メソッド内で設定しても効果が及びません。

「@backupStaticAttributes」 で説明する @backupStaticAttributes アノテーションを使うと、 宣言されたクラス内のすべての static プロパティの値をバックアップしてからテストを始め、 テストが終わった後でそれらの値を復元することができます。

テストが始まる際に、宣言されたすべてのクラスについて処理を行います。テストクラス自身だけではありません。 処理するのは、クラスの static プロパティだけです。関数の内部の static 変数は対象外です。

注記

@backupStaticAttributes の操作は、テストメソッドの前に実行されます。 ただし、有効になっている場合だけです。 先に実行されたテストメソッドの中で static プロパティの値が変更されており、かつそのメソッドでは @backupStaticAttributes が有効になっていなかった場合、 バックアップ (そしてリストア) されるのは、先に実行されたメソッドで変更後の値になります。 もともと宣言されていたデフォルト値ではありません。 PHP は、static 変数が宣言された当時のデフォルト値を、どこにも記録していないのです。

これは、テストの内部で新しく読み込んだ (宣言した) クラスの static プロパティについても同様です。 この場合も、もともと宣言されていたデフォルト値を、テストの後に復元することはできません。デフォルト値が残っていないからです。 テスト内で設定された値が、それ以降のテストに持ち越されます。

ユニットテストでは、テスト対象の static プロパティの値は、setUp() で明示的にリセットしておくことを推奨します (そして、tearDown() でもリセットしておけば、それ以降のテストに影響を及ぼすこともなくなります)。

static 属性のブラックリストを渡せば、保存と復元の対象からそれらを除外することもできます。 ブラックリストは、このように指定します。

class MyTest extends TestCase
{
    protected $backupStaticAttributesBlacklist = [
        'className' => ['attributeName']
    ];

    // ...
}

注記

$backupStaticAttributesBlacklist プロパティをたとえば setUp() メソッド内で設定しても効果が及びません。

第5章 テストの構成

PHPUnit の目指すところのひとつに 「自由に組み合わせられる」ということがあります。つまり、 例えば「そのプロジェクトのすべてのテストを実行する」「プロジェクトの中の ある部品を構成するすべてのクラスについて、すべてのテストを実行する」 「特定のひとつのクラスのテストのみを実行する」など、 数や組み合わせにとらわれずに好きなテストを一緒に実行できるということです。

PHPUnit では、さまざまな方法でテストを組み合わせてテストスイートにまとめることができます。 本章では、その中でもよく使われる手法を説明します。

ファイルシステムを用いたテストスイートの構成

おそらく、テストスイートをとりまとめるもっとも簡単な方法は すべてのテストケースのソースファイルを一つのテストディレクトリにまとめることでしょう。 PHPUnit はテストディレクトリを再帰的に探索し、 テストを自動的に見つけて実行します。

sebastianbergmann/money ライブラリのテストスイートを見てみましょう。このプロジェクトのディレクトリ構成を見ると、 テストケースクラスが tests ディレクトリにまとめられていることがわかります。 その中のディレクトリの構造は、テスト対象のシステム (SUT) がある src ディレクトリ以下の構造と同じになっています。

src                                 tests
`-- Currency.php                    `-- CurrencyTest.php
`-- IntlFormatter.php               `-- IntlFormatterTest.php
`-- Money.php                       `-- MoneyTest.php
`-- autoload.php

PHPUnit のコマンドラインテストランナーに テストディレクトリの場所を指示してやるだけで、 このライブラリのすべてのテストを実行できます。

phpunit --bootstrap src/autoload.php tests
PHPUnit 6.1.0 by Sebastian Bergmann.

.................................

Time: 636 ms, Memory: 3.50Mb

OK (33 tests, 52 assertions)

注記

PHPUnit のコマンドラインテストランナーでディレクトリを指定すると、 その中の *Test.php ファイルを見つけて実行します。

tests/CurrencyTest.php にあるテストケースクラス CurrencyTest で宣言されているテストだけを実行するには、 次のコマンドを実行します。

phpunit --bootstrap src/autoload.php tests/CurrencyTest
PHPUnit 6.1.0 by Sebastian Bergmann.

........

Time: 280 ms, Memory: 2.75Mb

OK (8 tests, 8 assertions)

実行したいテストをより細かく指示するには --filter オプションを使います。

phpunit --bootstrap src/autoload.php --filter testObjectCanBeConstructedForValidConstructorArgument tests
PHPUnit 6.1.0 by Sebastian Bergmann.

..

Time: 167 ms, Memory: 3.00Mb

OK (2 test, 2 assertions)

注記

この方式の欠点は、テストの実行順を制御できないことです。 そのため、テストの依存性に関する問題を引き起こすことがあります。 「テストの依存性」 を参照ください。 次の節では、テストの実行順序を XML 設定ファイルで明示的に指定する方法を説明します。

XML 設定ファイルを用いたテストスイートの構成

PHPUnit の XML 設定ファイル (付録 C) を使ってテストスイートを構成することもできます。 例 5.1 に、最小限の phpunit.xml ファイルを示します。これは、 tests ディレクトリを再帰的に探索して *Test.php というファイルにある *Test クラスをすべて追加する設定です。

例 5.1: XML 設定ファイルを用いたテストスイートの構成

<phpunit bootstrap="src/autoload.php">
  <testsuites>
    <testsuite name="money">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

--configuration が設定 されていない 場合は、現在の作業ディレクトリから phpunit.xml あるいは phpunit.xml.dist を (この順に) 探し、 見つかった場合はそれを自動的に読み込みます。

どのテストを実行するのかを明示的に指定することができます。

例 5.2: XML 設定ファイルを用いたテストスイートの構成

<phpunit bootstrap="src/autoload.php">
  <testsuites>
    <testsuite name="money">
      <file>tests/IntlFormatterTest.php</file>
      <file>tests/MoneyTest.php</file>
      <file>tests/CurrencyTest.php</file>
    </testsuite>
  </testsuites>
</phpunit>

第6章 リスクを伴うテスト

PHPUnit は、テストを実行する際に、以下のような追加のチェックをすることができます。

無意味なテスト

PHPUnit は、何も確かめていないテストを検出することができます。 このチェックを有効にするには、コマンドラインオプション --report-useless-tests を使うか、あるいは PHPUnit の XML 設定ファイルで beStrictAboutTestsThatDoNotTestAnything="true" を設定します。

何もアサーションを実行していないテストは、このチェックを有効にしておくと、 危険であるとマークされます。モックオブジェクトでの例外や、 @expectedException などのアノテーションは、アサーションとみなします。

意図せぬうちにカバーされているコード

PHPUnit は、意図せずカバーされているコードを検出することができます。 このチェックを有効にするには、コマンドラインオプション --strict-coverage を使うか、あるいは PHPUnit の XML 設定ファイルで checkForUnintentionallyCoveredCode="true" を設定します。

@covers アノテーションつきのテストが、 @covers@uses に記されていないコードを実行している場合に、 このチェックを有効にしておくと、危険であるとマークされます。

テストの実行時の出力

PHPUnit は、テストの最中の出力を検出することができます。 このチェックを有効にするには、コマンドラインオプション --disallow-test-output を使うか、あるいは PHPUnit の XML 設定ファイルで beStrictAboutOutputDuringTests="true" を設定します。

テストコードあるいは被テストコードの中で、print などで何かを出力している場合に、 このチェックを有効にしておくと、危険であるとマークされます。

テストの実行時のタイムアウト

PHP_Invoker パッケージがインストールされており、 かつ pcntl 拡張モジュールが利用可能な場合は、 テストの実行時間に制限を設けることができます。 この時間制限を有効にするには、コマンドラインオプション --enforce-time-limit を使うか、あるいは PHPUnit の XML 設定ファイルで beStrictAboutTestSize="true" を設定します。

@large とマークされたテストは、 実行時間が 60 秒を超えたら失敗します。 このタイムアウト時間は、XML 設定ファイルの timeoutForLargeTests 属性で変更できます。

@medium とマークされたテストは、 実行時間が 10 秒を超えたら失敗します。 このタイムアウト時間は、XML 設定ファイルの timeoutForMediumTests 属性で変更できます。

@medium とも @large ともマークされていないテストは、 @small とマークされたものとみなします。 このテストは、実行時間が 1 秒を超えたら失敗します。 このタイムアウト時間は、XML 設定ファイルの timeoutForSmallTests 属性で変更できます。

グローバルな状態の変更

PHPUnit は、グローバルな状態を変更するテストを厳格にチェックすることができます。 このチェックを有効にするには、コマンドラインで --strict-global-state オプションを指定するか、PHPUnit の XML 設定ファイルで beStrictAboutChangesToGlobalState="true" を指定します。

第7章 不完全なテスト・テストの省略

不完全なテスト

新しいテストケースクラスを作成する際には、 これから書くべきテストの内容をはっきりさせるために、 まず最初は以下のような空のテストメソッドを書きたくなることでしょう。

public function testSomething()
{
}

しかし、PHPUnit フレームワークでは空のメソッドを「成功した」 と判断してしまうという問題があります。このような解釈ミスがあると、 テスト結果のレポートが無意味になってしまいます。 そのテストがほんとうに成功したのか、 それともまだテストが実装されていないのかが判断できないからです。 実装していないテストメソッドの中で $this->fail() をコールするようにしたところで事態は何も変わりません。 こうすると、テストが「失敗した」と判断されてしまいます。 これは未実装のテストが「成功」と判断されてしまうのと同じくらいまずいことです (訳注: レポートを見ても、そのテストがほんとうに失敗したのか、 まだ実装されていないだけなのかがわかりません)。

テストの成功を青信号、失敗を赤信号と考えるなら、 テストが未完成あるいは未実装であることを表すための黄信号が必要です。 そのような場合に使用するインターフェイスが PHPUnit_Framework_IncompleteTest で、 これは未完成あるいは未実装のテストメソッドで発生する例外を表すものです。 このインターフェイスの標準的な実装が PHPUnit_Framework_IncompleteTestError です。

例 7.1 では SampleTest というテストケースクラスを定義しています。 便利なメソッド markTestIncomplete() (これは、自動的に PHPUnit_Framework_IncompleteTestError を発生させます) をテストメソッド内でコールすることで、 このメソッドがまだ完成していないことをはっきりさせます。

例 7.1: テストに未完成の印をつける

<?php
use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
    public function testSomething()
    {
        // オプション: お望みなら、ここで何かのテストをしてください。
        $this->assertTrue(true, 'これは動いているはずです。');

        // ここで処理を止め、テストが未完成であるという印をつけます。
        $this->markTestIncomplete(
          'このテストは、まだ実装されていません。'
        );
    }
}
?>

未完成のテストは、PHPUnit のコマンドライン版テストランナーでは以下のように I で表されます。

phpunit --verbose SampleTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

I

Time: 0 seconds, Memory: 3.95Mb

There was 1 incomplete test:

1) SampleTest::testSomething
このテストは、まだ実装されていません。

/home/sb/SampleTest.php:12
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 1, Incomplete: 1.

表 7.1 に、テストを未完成扱いにするための API を示します。

表7.1 未完成のテスト用の API

メソッド意味
void markTestIncomplete()現在のテストを未完成扱いにします。
void markTestIncomplete(string $message)現在のテストを未完成扱いにします。それを説明する文字列として $message を使用します。

テストの省略

すべてのテストがあらゆる環境で実行できるわけではありません。 考えてみましょう。たとえば、データベースの抽象化レイヤーを使用しており、 それがさまざまなドライバを使用してさまざまなデータベースシステムを サポートしているとします。MySQL ドライバのテストができるのは、 当然 MySQL サーバが使用できる環境だけです。

例 7.2 に示すテストケースクラス DatabaseTest には、 テストメソッド testConnection() が含まれています。 このクラスのテンプレートメソッド setUp() では、 MySQLi 拡張モジュールが使用可能かを調べたうえで、もし使用できない場合は markTestSkipped() メソッドでテストを省略するようにしています。

例 7.2: テストを省略する

<?php
use PHPUnit\Framework\TestCase;

class DatabaseTest extends TestCase
{
    protected function setUp()
    {
        if (!extension_loaded('mysqli')) {
            $this->markTestSkipped(
              'MySQLi 拡張モジュールが使用できません。'
            );
        }
    }

    public function testConnection()
    {
        // ...
    }
}
?>

飛ばされたテストは、PHPUnit のコマンドライン版テストランナーでは以下のように S で表されます。

phpunit --verbose DatabaseTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

S

Time: 0 seconds, Memory: 3.95Mb

There was 1 skipped test:

1) DatabaseTest::testConnection
MySQLi 拡張モジュールが使用できません。

/home/sb/DatabaseTest.php:9
OK, but incomplete or skipped tests!
Tests: 1, Assertions: 0, Skipped: 1.

表 7.2 に、テストを省略するための API を示します。

表7.2 テストを省略するための API

メソッド意味
void markTestSkipped()現在のテストを省略扱いにします。
void markTestSkipped(string $message)現在のテストを省略扱いにします。それを説明する文字列として $message を使用します。

@requires によるテストのスキップ

ここまでに示したメソッドに加えて、 @requires アノテーションを使って共通の事前条件を記述することもできます。

表7.3 @requires の例用例

取り得る値別の例
PHPPHP のバージョン@requires PHP 5.3.3@requires PHP 7.1-dev
PHPUnitPHPUnit のバージョン@requires PHPUnit 3.6.3@requires PHPUnit 4.6
OSPHP_OS にマッチする正規表現@requires OS Linux@requires OS WIN32|WINNT
functionfunction_exists に渡せるパラメータ@requires function imap_open@requires function ReflectionMethod::setAccessible
extension拡張モジュール名 (オプションでバージョンも指定できる)@requires extension mysqli@requires extension redis 2.2.0

例 7.3: @requires を使ったテストケースのスキップ

<?php
use PHPUnit\Framework\TestCase;

/**
 * @requires extension mysqli
 */
class DatabaseTest extends TestCase
{
    /**
     * @requires PHP 5.3
     */
    public function testConnection()
    {
        // このテストには mysqli 拡張モジュールと PHP 5.3 以降が必須です
    }

    // ... その他のすべてのテストには mysqli 拡張モジュールが必須です
}
?>

特定のバージョンの PHP でしか使えない構文を利用する場合は、 「テストスイート」 にあるように XML 設定ファイルでのバージョン依存のインクルードを検討しましょう。

第8章 データベースのテスト

初級者・中級者向けのユニットテストのサンプルは、 どんな言語を対象としたものであっても、 テストしやすいようなロジックに対してシンプルなテストをしているものばかりです。 データベースを扱う一般的なアプリケーションを考えると、これはまったく現実離れしています。 たとえば Wordpress や TYPO3、あるいは Symfony で Doctrine や Propel などを使い始めるとすぐに、 PHPUnit でのテストがやりづらいことを実感するはずです。 データベースとこれらのライブラリが密結合になっているからです。

注記

PHP の pdo 拡張モジュール、そしてデータベースごとの拡張モジュール (pdo_mysql など) がインストールされていることを確認しておきましょう。 これらがインストールされていないと、以下のサンプルは動きません。

きっと日々の業務やプロジェクトでも身に覚えがあることでしょう。 自分の持つ PHPUnit に関する知識を駆使して作業を進めようとしたのに、 こんな問題のせいで行き詰ってしまうことが。

  1. テストしたいメソッドがかなり大きめの JOIN 操作を実行し、 データを使って重要な結果を算出している。

  2. ひとつのビジネスロジックの中で SELECT、INSERT、UPDATE そして DELETE を組み合わせて実行している。

  3. ふたつ以上の (おそらくもっと多い) テーブルから初期データを準備しないと そのメソッドのテストができない。

DBUnit 拡張を使うと、テスト用のデータベースのセットアップを単純化でき、 データベース操作後の内容の検証もすることができます。

データベースのテストに対応しているベンダー

DBUnit が現在サポートしているのは、MySQL および PostgreSQL、Oracle、SQLite です。 Zend FrameworkDoctrine 2 を使うと、IBM DB2 や Microsoft SQL Server のような他のデータベースにもアクセスできます。

データベースのテストの難しさ

ウェブ上にあるユニットテストのサンプルの中にデータベースを扱うものが全く見当たらない理由はなぜか。 それは、データベースを扱うテストは準備するのも保守するのもたいへんだからです。 データベースを使うテストをするには、このようなことに気をつける必要があります。

  • データベースのスキーマやテーブル

  • テーブルへの、テストで必要となるレコードの追加

  • テスト実行後のデータベースの状態の検証

  • テスト実行ごとのデータベースの後始末

PDO や MySQLi あるいは OCI8 といったデータベース API はどれも使いにくい上に、 こういった処理を自分で書こうとすると長ったらしくなってしまって面倒です。

テストコードはできる限り簡潔に、そして明確に書かねばなりません。その理由は次のとおりです。

  • 製品コードにちょっと手を加えるたびに大量のテストコードを変更する羽目になるのは困る。

  • 数ヵ月後に改めて読み直したときにも 読みやすく理解しやすいテストコードであってほしい。

さらに知っておく必要があることは、 データベースは基本的に、自分のコードへのグローバルな入力変数であるということです。 テストスイート内にあるふたつのテストを同じデータベースに対して実行すると、 おそらくデータを複数回再利用することになります。あるテストが失敗すると それ以降のテストの結果にも影響を及ぼしやすく、テストを進めるのが非常に難しくなります。 先ほど箇条書きでまとめた中の「後始末」こそが、この データベースがグローバルな入力になる 問題を解決するために重要です。

DbUnit を使うと、 データベースのテストにおけるこれらの問題をシンプルにする助けになります。

PHPUnit では助けようにもどうにもならないことが、 データベースのテストはデータベースを使わないものに比べてとても遅くなるという事実です。 テストの実行時間がどれくらいになるかはデータベースとのやりとりの量に依存しますが、 各テストで使うデータの量を少なめにしておいて 可能な限りはデータベースを使わないテストで済ませるようにすれば、 巨大なテストスイートであっても 1 分未満で実行させるのは容易です。

Doctrine 2 プロジェクト がよい例です。 このプロジェクトのテストスイートには現時点で約 1000 件のテストが含まれています。 そのほぼ半数がデータベースを扱うテストですが、 標準的なデスクトップコンピューター上の MySQL を使ってテストスイートを実行しても 15 秒程度でテストが完了します。

データベーステストの四段階

Gerard Meszaros は、著書 xUnit Test Patterns でユニットテストを次の四段階に分類しています。

  1. フィクスチャのセットアップ (Setup)

  2. テストしたいシステムの実行 (Exercise)

  3. 結果の検証 (Verify)

  4. 後始末 (Teardown)

フィクスチャとは?

フィクスチャとは、アプリケーションやデータベースの初期状態のことです。 テストを実行する前に用意します。

データベースをテストするには、少なくとも setup と teardown のときにはテーブルに接続してフィクスチャのクリーンアップや書き込みをしなければなりません。 しかし、データベース拡張には、 データベーステストの四段階を次のようなワークフローに振り向ける十分な理由があります。 このフローは、個々のテストに対して実行します。

1. データベースのクリーンアップ

データベースを扱う最初のテストというのはいつでも存在します。 実際のところ、そのときテーブルにデータが存在するのかどうかはわかりません。 PHPUnit は指定した全テーブルに対して TRUNCATE を実行し、 テーブルの中身を空にします。

2. フィクスチャの準備

その後、PHPUnit はフィクスチャの各行を順次処理し、対応するテーブルに書き込みます。

3–5. テストの実行、結果の検証、そして後始末

データベースをリセットして初期状態を読み込んだら、 実際のテストを PHPUnit が実行します。 テストコードのこの部分は Database Extension の存在を知っている必要はなく、 コードに対してなんでもお好みのテストをすることができます。

テストの中で assertDataSetsEqual() という特殊なアサーションを使って検証しているかもしれません。 しかし、この機能は完全なオプションです。 この機能は データベースアサーション で説明します。

PHPUnit のデータベーステストケースの設定

通常、PHPUnit を使うテストケースでは PHPUnit\Framework\TestCase クラスを継承してこのようにします。

<?php
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    public function testCalculate()
    {
        $this->assertEquals(2, 1 + 1);
    }
}
?>

テストコードで Database Extension を使う場合は少しだけ複雑になり、 別の抽象テストケースを継承しなければなりません。そして、二つの抽象メソッド getConnection()getDataSet() を実装します。

<?php
class MyGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        $pdo = new PDO('sqlite::memory:');
        return $this->createDefaultDBConnection($pdo, ':memory:');
    }

    /**
     * @return PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    public function getDataSet()
    {
        return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/guestbook-seed.xml');
    }
}
?>

getConnection() の実装

クリーンアップとフィクスチャの読み込みの機能を動かすには、 PHPUnit Database Extension からデータベース接続にアクセスできなければなりません。 データベース接続の抽象化には PDO ライブラリを使います。 重要なのは、PHPUnit のデータベース拡張を使うためだけに わざわざアプリケーションを PDO ベースにする必要はないということです。 この接続を使うのは、単にクリーンアップとフィクスチャの準備のためだけです。

先ほどの例では、インメモリの SQLite 接続を作って createDefaultDBConnection メソッドに渡しました。 このメソッドは PDO のインスタンスをラップしたもので、二番目のパラメータ (データベース名) に非常にシンプルなデータベース接続の抽象化レイヤーを渡します。このパラメータの型は PHPUnit_Extensions_Database_DB_IDatabaseConnection です。

データベース接続の使い方で、このインターフェイスの API と、その活用法について説明します。

getDataSet() の実装

getDataSet() メソッドで定義するのは、 個々のテストを実行する前のデータベースの初期状態がどうあるべきかということです。 データベースの状態の抽象化は DataSet と DataTable という概念を使って行い、これらをそれぞれ PHPUnit_Extensions_Database_DataSet_IDataSet および PHPUnit_Extensions_Database_DataSet_IDataTable というインターフェイスで表します。次の節でこれらの概念を詳しく説明し、 これをデータベースのテストに使うと何がうれしいのかについても示します。

実装するために最低限知っておくべきことは、 getDataSet() メソッドがコールされるのが setUp() の中で一度だけであり、 ここでフィクスチャのデータセットを取得してデータベースに挿入するということです。 先ほどの例では、ファクトリメソッド createFlatXMLDataSet($filename) を使って XML 形式のデータセットを表しました。

データベーススキーマ (DDL) とは?

PHPUnit は、テストの実行前にデータベーススキーマ (すべてのテーブル、トリガー、シーケンス、ビューを含むもの) ができあがっていることを想定しています。つまり開発者としては、 テストスイートを実行する前にデータベースを正しく準備しておかねばならないということです。

データベースのテストにおけるこの事前条件を満たす方法には、次のようなものがあります。

  1. インメモリの SQLite ではなく永続化したデータベースを使うのなら、 最初に一度 phpMyAdmin (MySQL の場合) などのツールでデータベースを用意しておけば、 あとはテストを実行するたびにそれを再利用できます。

  2. Doctrine 2Propel といったライブラリを使っている場合は、その API を使えばテストの実行前に必要なデータベーススキーマを作ることができます。 PHPUnit のブートストラップ 機能を使うと、そのコードをテスト実行時に毎回実行させることもできます。

ヒント: 自前でのデータベーステストケースの抽象化

先の実装例を見ればすぐにわかるでしょうが、 getConnection() メソッドはきわめて静的なものであり、 さまざまなデータベーステストケースで再利用することができます。 さらに、テストのパフォーマンスを良好に保ちつつデータベースのオーバーヘッドを下げるために、 ちょっとしたリファクタリングを施して汎用的な抽象テストケースを用意しましょう。 このようにしても、テストケースごとに異なるデータフィクスチャを指定することができます。

<?php
abstract class MyApp_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
    // PDO のインスタンス生成は、クリーンアップおよびフィクスチャ読み込みのときに一度だけ
    static private $pdo = null;

    // PHPUnit_Extensions_Database_DB_IDatabaseConnection のインスタンス生成は、テストごとに一度だけ
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO('sqlite::memory:');
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, ':memory:');
        }

        return $this->conn;
    }
}
?>

しかし、これはまだデータベースへの接続情報を PDO 接続の設定にハードコードしてしまっています。 PHPUnit にはさらにすばらしい機能があるので、それを使ってテストケースをより汎用的にしましょう。 XML 設定ファイル を使えば、テストの実行のたびにデータベース接続を設定できます。 まずは phpunit.xml というファイルをアプリケーションの tests/ ディレクトリに作り、 中身をこのようにします。

<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
    <php>
        <var name="DB_DSN" value="mysql:dbname=myguestbook;host=localhost" />
        <var name="DB_USER" value="user" />
        <var name="DB_PASSWD" value="passwd" />
        <var name="DB_DBNAME" value="myguestbook" />
    </php>
</phpunit>

テストケースはこのように書き直せます。

<?php
abstract class Generic_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
    // PDO のインスタンス生成は、クリーンアップおよびフィクスチャ読み込みのときに一度だけ
    static private $pdo = null;

    // PHPUnit_Extensions_Database_DB_IDatabaseConnection のインスタンス生成は、テストごとに一度だけ
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO( $GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD'] );
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
        }

        return $this->conn;
    }
}
?>

データベースの設定情報を切り替えてテストスイートを実行するには、 コマンドラインから次のようにします。

user@desktop> phpunit --configuration developer-a.xml MyTests/
user@desktop> phpunit --configuration developer-b.xml MyTests/

データベースのテストを実行するときにターゲットデータベースを切り替えられるようにしておくことは、 開発機で作業をしている場合などは特に重要です。 複数の開発者が同じデータベース接続を使ってデータベースのテストを実行したりすると、 レースコンディション (競合条件) によるテストの失敗が頻発するでしょう。

データセットとデータテーブルについて知る

PHPUnit Database Extension の中心となる概念が データセットとデータテーブルです。まずはこの考え方を理解することが、 PHPUnit でのデータベースのテストをマスターする近道です。 データセットとデータテーブルは、データベースのテーブルや行、 そしてカラムの抽象化レイヤーです。シンプルな API によってデータベースの内容をオブジェクト構造に隠蔽できるだけでなく、 データベース以外のソースによる実装もできるようになっています。

この抽象化を使って、データベースの実際の中身と我々が期待する内容を比較します。 期待する内容は XML や YAML そして CSV などのファイルでも表せますし、 PHP の配列として表すこともできます。 DataSet インターフェイスと DataTable インターフェイスのおかげで、 これらの全く異なる概念のソースをリレーショナルデータベースに見立てて 同様に扱えるようになります。

データベースのアサーションをテストの中で行う流れは、 次のようにシンプルな三段階となります。

  • ひとつあるいは複数のテーブルをデータベース内から指定する (実際のデータセット)。

  • 期待するデータセットをお好みのフォーマット (YAML, XML など) で用意する。

  • 両者がお互いに等しいことを確認する。

データセットやデータテーブルの PHPUnit Database Extension における使い道は、 何もアサーションだけだというわけではありません。先ほどの節で見たように、 これらを使ってデータベースの初期状態の内容を記述することもできます。 フィクスチャとなるデータセットを Database TestCase で定義すると、それをこのように使うことができます。

  • データセットで指定したテーブルのすべての行を削除する。

  • データテーブルのすべての行をデータベースに書き込む。

利用できる実装

これら三種類のデータセット/データテーブルが用意されています。

  • ファイルベースのデータセットやデータテーブル

  • クエリベースのデータセットやデータテーブル

  • フィルタ用や合成用のデータセットやデータテーブル

ファイルベースのデータセットやデータテーブルは、 初期状態のフィクスチャを定義したり期待する状態を定義したりするときによく使います。

フラット XML データセット

最も一般的なデータセットは、フラット XML と呼ばれるものです。 これは非常にシンプルな xml 形式で、ルートノード <dataset> の中のタグがデータベースのひとつの行を表します。 テーブルと同じ名前のタグが追加する行を表し、 その属性がカラムを表します。 単純な掲示板アプリケーションの例は、このようになります。

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" />
</dataset>

見るからに書きやすそうですね。この場合は <guestbook> がテーブル名で、 2 行が追加されます。そして、四つのカラム idcontentuser そして created に、それぞれ対応する値が設定されています。

しかし、この単純性による問題もあります。

たとえば、先ほどの例で空のテーブルはどうやって指定すればいいのかがよくわかりません。 実は、何も属性を指定せずにテーブルと同じ名前のタグを追加すれば、空のテーブルを表すことができます。 空の guestbook テーブルを表すフラット xml ファイルは、このようになります。

<?xml version="1.0" ?>
<dataset>
    <guestbook />
</dataset>

フラット xml データセットでの NULL 値の処理は、あまりおもしろいものではありません。 ほとんどのデータベースでは、NULL 値と空文字列は別のものとして扱います (例外のひとつは Oracle です) が、これをフラット xml 形式で表すのは困難です。NULL 値を表すには、 行の指定のときに属性を省略します。 この例の掲示板で、匿名の投稿を許可し、そのときには user カラムに NULL を指定することにしましょう。 guestbook テーブルの状態は、このようになります。

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" created="2010-04-26 12:14:20" />
</dataset>

この例では、二番目のエントリが匿名の投稿を表します。 しかし、これはカラムの認識において深刻な問題につながります。 データセットが等しいことを確認するアサーションでは、各データセットでテーブルの持つカラムを指定しなければなりません。 ある属性がデータテーブルのすべての行で NULL だったなら、 Database Extension はそのカラムがテーブルに存在することをどうやって知るというのでしょう?

フラット XML データセットはここで、重大な前提を使っています。 テーブルの最初の行で定義されている属性が、そのテーブルのカラムを定義しているものと見なすのです。 先ほどの例では、guestbook テーブルのカラムが idcontentuser そして created であると見なすということです。二番目の行には user が定義されていないので、データベースには NULL を挿入します。

guestbook の最初のエントリをデータセットから削除すると、guestbook テーブルのカラムは idcontent そして created だけになってしまいます。 user が指定されていないからです。

フラット XML データセットを効率的に使うには、NULL 値がからむ場合は 各テーブルの最初の行には NULL を含まないようにします。 それ以降の行では、属性を省略して NULL を表すことができます。 これはあまりスマートなやり方ではありません。 というのも、データベースのアサーションで行の順番が影響してしまうからです。

一方、テーブルのカラムの一部だけをフラット XML データセットで指定すると、 それ以外のカラムにはデフォルト値が設定されます。 そのため、もし省略したカラムの定義が NOT NULL DEFAULT NULL などの場合はエラーになります。

結論として言えるのは、フラット XML データセットを使うなら NULL 値が不要な場合だけにしておいたほうがよい、ということだけです。

フラット XML データセットのインスタンスを Database TestCase から作るには、 createFlatXmlDataSet($filename) メソッドを使います。

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
    }
}
?>

XML データセット

もうひとつ別の構造の XML データセットもあります。これは多少冗長な書き方ですが、 フラット XML データセットにおける NULL の問題は発生しません。 ルートノード <dataset> の配下に指定できるタグは、 <table><column><row><value> そして <null /> です。 先に定義した Guestbook のフラット XML と同様のデータセットは、このようになります。

<?xml version="1.0" ?>
<dataset>
    <table name="guestbook">
        <column>id</column>
        <column>content</column>
        <column>user</column>
        <column>created</column>
        <row>
            <value>1</value>
            <value>Hello buddy!</value>
            <value>joe</value>
            <value>2010-04-24 17:15:23</value>
        </row>
        <row>
            <value>2</value>
            <value>I like it!</value>
            <null />
            <value>2010-04-26 12:14:20</value>
        </row>
    </table>
</dataset>

<table> には name が必須で、 さらにすべてのカラムの名前を定義しなければなりません。 また、ゼロ個以上の <row> 要素を含めることができます。<row> 要素を定義しなければ、そのテーブルが空であることになります。 <value> タグや <null /> タグは、先に指定した column> 要素の順番で指定しなければなりません。 <null /> タグは、 見た目の通り、値が NULL であることを表します。

XML データセットのインスタンスを Database TestCase から作るには、 createXmlDataSet($filename) メソッドを使います。

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createXMLDataSet('myXmlFixture.xml');
    }
}
?>

MySQL XML データセット

この新しい XML フォーマットは、 MySQL データベース 専用です。 PHPUnit 3.5 以降で対応します。この形式のファイルを生成するには、 mysqldump を使います。mysqldump では CSV データセットも対応していますが、 それとは違ってこの XML 形式の場合はひとつのファイルに複数のテーブルを含めることができます。 この形式のファイルを作るには、 mysqldump を次のように実行します。

mysqldump --xml -t -u [username] --password=[password] [database] > /path/to/file.xml

このファイルを Database TestCase で使うには、 createMySQLXMLDataSet($filename) メソッドをコールします。

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createMySQLXMLDataSet('/path/to/file.xml');
    }
}
?>

YAML データセット

あるいは、YAML データセットを使って、guestbook の例をこのように表すこともできます。

guestbook:
  -
    id: 1
    content: "Hello buddy!"
    user: "joe"
    created: 2010-04-24 17:15:23
  -
    id: 2
    content: "I like it!"
    user:
    created: 2010-04-26 12:14:20

これは、シンプルで便利なうえに、さらにフラット XML データセットが持つ NULL の問題も解決しています。 NULL を YAML で表すには、単にカラム名の後に何も値を指定しなければよいのです。 空文字列を指定する場合は column1: "" のようにします。

YAML Dataset 用のファクトリーメソッドは今のところ Database TestCase に存在しないので、手動でインスタンスを生成しなければなりません。

<?php
class YamlGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        return new PHPUnit_Extensions_Database_DataSet_YamlDataSet(
            dirname(__FILE__)."/_files/guestbook.yml"
        );
    }
}
?>

CSV データセット

さらにもうひとつのファイルベースのデータセットとして、CSV ファイルを使ったものもあります。データセット内の各テーブルを、 それぞれ単一の CSV ファイルとして扱います。 guestbook の例では、このようなファイル guestbook-table.csv を定義します。

id,content,user,created
1,"Hello buddy!","joe","2010-04-24 17:15:23"
2,"I like it!","nancy","2010-04-26 12:14:20"

この形式は Excel や OpenOffice で編集できるという点で非常に便利ですが、 CSV データセットでは NULL 値を指定することができません。 空のカラムは、データベースのデフォルトに基づいた空の値として扱われます。

CSV データセットを作るには、このようにします。

<?php
class CsvGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
        $dataSet->addTable('guestbook', dirname(__FILE__)."/_files/guestbook.csv");
        return $dataSet;
    }
}
?>

Array データセット

PHPUnit の Database Extension のバージョン 1.3.2 以降では、 配列ベースのデータセットが使えます。 guestbook の例だと、このようになります。

<?php
class ArrayGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        return new MyApp_DbUnit_ArrayDataSet(
            [
                'guestbook' => [
                    [
                        'id' => 1,
                        'content' => 'Hello buddy!',
                        'user' => 'joe',
                        'created' => '2010-04-24 17:15:23'
                    ],
                    [
                        'id' => 2,
                        'content' => 'I like it!',
                        'user' => null,
                        'created' => '2010-04-26 12:14:20'
                    ],
                ],
            ]
        );
    }
}
?>

PHP の DataSet には、これまでのファイルベースのデータセットに比べて明らかな利点があります。

  • PHP の配列は NULL 値を扱える。

  • アサーション用に新たなファイルを用意する必要がなく、 直接テストケース内で指定できる。

このデータセットでは、フラット XML や CSV そして YAML データセットと同様に、最初に指定した行のキーがテーブルのカラム名を表します。 つまり、先ほどの例だと idcontentuser そして created です。

このデータセットの実装は、シンプルでわかりやすいものです。

<?php
class MyApp_DbUnit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet
{
    /**
     * @var array
     */
    protected $tables = [];

    /**
     * @param array $data
     */
    public function __construct(array $data)
    {
        foreach ($data AS $tableName => $rows) {
            $columns = [];
            if (isset($rows[0])) {
                $columns = array_keys($rows[0]);
            }

            $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
            $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);

            foreach ($rows AS $row) {
                $table->addRow($row);
            }
            $this->tables[$tableName] = $table;
        }
    }

    protected function createIterator($reverse = false)
    {
        return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
    }

    public function getTable($tableName)
    {
        if (!isset($this->tables[$tableName])) {
            throw new InvalidArgumentException("$tableName is not a table in the current database.");
        }

        return $this->tables[$tableName];
    }
}
?>

Query (SQL) データセット

データベースのアサーションでは、ファイルベースのデータセットだけでなく Query/SQL ベースのデータセットでデータベースの実際の中身を含むものが必要になることもあります。 そんなときに使えるのが Query データセットです。

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook');
?>

単にテーブル名だけを指定してテーブルを追加すると、 次のクエリを実行してデータテーブルを定義したのと同じ意味になります。

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook', 'SELECT * FROM guestbook');
?>

ここでテーブルに対して任意のクエリを実行して、 取得する行や列を絞り込んだり ORDER BY 句を追加したりすることができます。

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook', 'SELECT id, content FROM guestbook ORDER BY created DESC');
?>

データベースアサーションの節で、このデータセットを使う方法をより詳しく説明しています。

Database (DB) データセット

テスト用のデータベース接続にアクセスすると、 自動的にすべてのテーブルとその中身を含むデータセットを生成します。 接続先のデータベースは、接続用のファクトリーメソッドの二番目のパラメータで指定します。

データベース全体の完全なデータセットを作るには testGuestbook() のようにします。 ホワイトリスト形式で指定したテーブルだけに絞り込むには testFilteredGuestbook() メソッドのようにします。

<?php
class MySqlGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        $database = 'my_database';
        $user = 'my_user';
        $password = 'my_password';
        $pdo = new PDO('mysql:...', $user, $password);
        return $this->createDefaultDBConnection($pdo, $database);
    }

    public function testGuestbook()
    {
        $dataSet = $this->getConnection()->createDataSet();
        // ...
    }

    public function testFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet($tableNames);
        // ...
    }
}
?>

Replacement データセット

これまで、フラット XML や CSV のデータセットには NULL の問題があると説明してきました。 しかし、ちょっとわかりにくい回避策を使えばこれらのデータセットで NULL を扱うこともできます。

Replacement データセットは既存のデータセットに対するデコレータで、 データセットの任意のカラムの値を別の値で置換することができます。 guestbook の例で NULL 値を扱うには、このようなファイルを作ります。

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" />
</dataset>

そして、フラット XML データセットを Replacement データセットでラップします。

<?php
class ReplacementTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        $ds = $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
        $rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds);
        $rds->addFullReplacement('##NULL##', null);
        return $rds;
    }
}
?>

データセットフィルタ

巨大なフィクスチャファイルを扱うときには、 データセットフィルタをホワイトリストあるいはブラックリストとして使って テーブルやカラムを絞り込んだサブデータセットを作ることができます。 これは、DB データセットと組み合わせて データセットのカラムを絞り込むときに使うと非常に便利です。

<?php
class DataSetFilterTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testIncludeFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet();

        $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
        $filterDataSet->addIncludeTables(['guestbook']);
        $filterDataSet->setIncludeColumnsForTable('guestbook', ['id', 'content']);
        // ..
    }

    public function testExcludeFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet();

        $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
        $filterDataSet->addExcludeTables(['foo', 'bar', 'baz']); // only keep the guestbook table!
        $filterDataSet->setExcludeColumnsForTable('guestbook', ['user', 'created']);
        // ..
    }
}
?>

注意 ひとつのテーブルに対してカラムの exclude フィルタと include フィルタを同時に使うことはできません。 さらに、テーブルのホワイトリストとブラックリストはどちらか一方しか指定できません。

Composite データセット

Composite データセットは、既存の複数のデータセットをひとつにまとめるときに有用です。 複数のデータセットに同名のテーブルが含まれる場合は、 指定した順で行を連結します。 たとえば、このようなふたつのデータセットがあるものとしましょう。 まずは fixture1.xml

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
</dataset>

そして fixture2.xml

<?xml version="1.0" ?>
<dataset>
    <guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" />
</dataset>

Composite データセットを使えば、両方のフィクスチャファイルをまとめることができます。

<?php
class CompositeTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        $ds1 = $this->createFlatXmlDataSet('fixture1.xml');
        $ds2 = $this->createFlatXmlDataSet('fixture2.xml');

        $compositeDs = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet();
        $compositeDs->addDataSet($ds1);
        $compositeDs->addDataSet($ds2);

        return $compositeDs;
    }
}
?>

外部キーには注意

フィクスチャを準備するとき、PHPUnit の Database Extension はフィクスチャ内で定義された順に行を追加していきます。 データベースのスキーマ定義で外部キーを使っている場合は、 外部キー制約に違反しないような順番でテーブルを指定しなければなりません。

自作のデータセットやデータテーブルの実装

データセットやデータテーブルの内部構造を理解するために、 まずはデータセットのインターフェイスから見ていきましょう。 自分でデータセットやデータテーブルを作るつもりのない人は、 読み飛ばしてもかまいません。

<?php
interface PHPUnit_Extensions_Database_DataSet_IDataSet extends IteratorAggregate
{
    public function getTableNames();
    public function getTableMetaData($tableName);
    public function getTable($tableName);
    public function assertEquals(PHPUnit_Extensions_Database_DataSet_IDataSet $other);

    public function getReverseIterator();
}
?>

公開インターフェイスは、データベーステストケースの assertDataSetsEqual() アサーションで内部的に使われており、これでデータセットの内容を検証します。 IDataSet は IteratorAggregate インターフェイスから getIterator() メソッドを継承しており、これを使ってデータセット内の全テーブルの反復処理を行います。 リバースイテレータを使うと、PHPUnit で作ったテーブルのデータの切り詰めを、 テーブルを作ったときと逆の順番で行えます。これで、外部キー制約に違反せずに済むようになります。

テーブルのインスタンスをデータセットに追加するには、 実装によってさまざまな手法があります。たとえば YamlDataSetXmlDataSet そして FlatXmlDataSet のようなファイルベースのデータセットでは、 データセットの作成時にソースファイルを使って内部的に追加します。

テーブルは、このようなインターフェイスを使って表します。

<?php
interface PHPUnit_Extensions_Database_DataSet_ITable
{
    public function getTableMetaData();
    public function getRowCount();
    public function getValue($row, $column);
    public function getRow($row);
    public function assertEquals(PHPUnit_Extensions_Database_DataSet_ITable $other);
}
?>

getTableMetaData() メソッドは別として、 それ以外のメソッドはまさに文字通りの働きをするものです。 これらのメソッドはすべて、Database Extension のさまざまなアサーションで必須となります。 その詳細は次の章で説明します。 getTableMetaData() メソッドの返す値は、 PHPUnit_Extensions_Database_DataSet_ITableMetaData インターフェイスを実装したものでなければなりません。 このインターフェイスはテーブルの構造を表し、このような情報を保持します。

  • テーブル名。

  • テーブルのカラム名の配列。並び順は、結果セットに登場する順と同じ。

  • 主キーカラムの配列。

このインターフェイスには、ふたつの TableMetaData のインスタンスがお互いに等しいかを調べるアサーションも定義されています。 これは、データセットの同一性を調べるアサーションで利用するものです。

接続 API

Connection インターフェイスには、三種類のおもしろいメソッドが用意されています。 このインターフェイスは、データベーステストケースの getConnection() メソッドが返すものです。

<?php
interface PHPUnit_Extensions_Database_DB_IDatabaseConnection
{
    public function createDataSet(Array $tableNames = NULL);
    public function createQueryTable($resultName, $sql);
    public function getRowCount($tableName, $whereClause = NULL);

    // ...
}
?>
  1. createDataSet() メソッドは、Database (DB) データセットを作ります。これは、データセットの実装の節で説明したものです。

    <?php
    class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCreateDataSet()
        {
            $tableNames = ['guestbook'];
            $dataSet = $this->getConnection()->createDataSet();
        }
    }
    ?>
  2. createQueryTable() メソッドを使うと、 QuryTable のインスタンスを作れます。引数には、結果の名前と SQL クエリを渡します。 これは、次の節 (データベースアサーション API) で説明する結果やテーブルのアサーションで有用なメソッドです。

    <?php
    class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCreateQueryTable()
        {
            $tableNames = ['guestbook'];
            $queryTable = $this->getConnection()->createQueryTable('guestbook', 'SELECT * FROM guestbook');
        }
    }
    ?>
  3. getRowCount() は、 テーブル内の行数を手軽に取得するためのメソッドです。 オプションで、where 句によるフィルタリングもできます。 これを使えば、シンプルな同一性のアサーションが可能です。

    <?php
    class ConnectionTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testGetRowCount()
        {
            $this->assertEquals(2, $this->getConnection()->getRowCount('guestbook'));
        }
    }
    ?>

データベースアサーション API

テストツール用として、Database Extension ではいくつかのアサーションを提供しています。 これらを使えば、データベースやテーブルの現在の状態 そしてテーブルの行数を検証できます。この節では、 これらの機能の詳細を説明します。

テーブルの行数のアサーション

テーブルの行数が特定の値であるかどうかを調べられれば便利なことがよくあります。 これは、接続 API を使ってちょっとしたコードを書かなくとも簡単に実現できます。 guestbook に行を追加した後で、初期登録した 2 エントリ以外にもう一行増えて 3 行になっていることを調べるには、このようにします。

<?php
class GuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testAddEntry()
    {
        $this->assertEquals(2, $this->getConnection()->getRowCount('guestbook'), "Pre-Condition");

        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $this->assertEquals(3, $this->getConnection()->getRowCount('guestbook'), "Inserting failed");
    }
}
?>

テーブルの状態のアサーション

先ほどのアサーションも有用ですが、本当にチェックしたいのは、 すべての値が正しいカラムにきちんと登録されたかどうかです。 これは、テーブルのアサーションで実現します。

そのために、QueryTable のインスタンスを定義しました。 テーブル名と SQL クエリからその内容を取得し、 それをファイルベースあるいは配列ベースのデータセットと比較します。

<?php
class GuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testAddEntry()
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );
        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>

さて次に、このアサーションに使うフラット XML ファイル expectedBook.xml を用意しましょう。

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" />
    <guestbook id="3" content="Hello world!" user="suzy" created="2010-05-01 21:47:08" />
</dataset>

残念ながら、このアサーションが成功するのは、ちょうど 2010–05–01 21:47:08 に実行したときだけになります。 日付はデータベースのテストでいつも問題になるものなので、それを回避する手段として created カラムをアサーションで無視させることができます。

調整後のフラット XML ファイル expectedBook.xml はこのようになり、これでアサーションを通過させることができます。

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" />
    <guestbook id="2" content="I like it!" user="nancy" />
    <guestbook id="3" content="Hello world!" user="suzy" />
</dataset>

QueryTable の呼び出しも修正しなければなりません。

<?php
$queryTable = $this->getConnection()->createQueryTable(
    'guestbook', 'SELECT id, content, user FROM guestbook'
);
?>

クエリの結果のアサーション

複雑なクエリの結果に対するアサーションも、 QueryTable 方式で可能です。単に結果の名前とクエリを指定して、 それをデータセットと比較すればよいのです。

<?php
class ComplexQueryTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testComplexQuery()
    {
        $queryTable = $this->getConnection()->createQueryTable(
            'myComplexQuery', 'SELECT complexQuery...'
        );
        $expectedTable = $this->createFlatXmlDataSet("complexQueryAssertion.xml")
                              ->getTable("myComplexQuery");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>

複数のテーブルの状態のアサーション

もちろん、複数のテーブルの状態を一度に確かめたり クエリデータセットをファイルベースのデータセットと比較したりすることも可能です。 データセットのアサーションには二通りの方法があります。

  1. 接続の Database (DB) データセットを使い、 それをファイルベースのデータセットと比較する。

    <?php
    class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testCreateDataSetAssertion()
        {
            $dataSet = $this->getConnection()->createDataSet(['guestbook']);
            $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml');
            $this->assertDataSetsEqual($expectedDataSet, $dataSet);
        }
    }
    ?>
  2. データセットを自分で作ることもできます。

    <?php
    class DataSetAssertionsTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function testManualDataSetAssertion()
        {
            $dataSet = new PHPUnit_Extensions_Database_DataSet_QueryDataSet();
            $dataSet->addTable('guestbook', 'SELECT id, content, user FROM guestbook'); // additional tables
            $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml');
    
            $this->assertDataSetsEqual($expectedDataSet, $dataSet);
        }
    }
    ?>

よくある質問

PHPUnit は、テストごとにデータベーススキーマを作り直すの?

いいえ。PHPUnit は、テストスイートの開始時にすべてのデータベースオブジェクトが存在することを前提とします。 データベースやテーブル、シーケンス、トリガー、そしてビューなどは、 テストスイートを実行する前に作っておく必要があります。

Doctrine 2eZ Components の強力なツールを使えば、定義済みのデータ構造からデータベーススキーマを作成できます。 しかし、これらを使うには PHPUnit extension にフックで組み込まねばなりません。 そうしないと、テストスイートを実行する前にデータベースの自動再作成ができなくなります。

各テストの実行後はデータベースをクリアするので、 テストを実行するたびにデータベースを再作成する必要はありません。 事前に作ったデータベースをずっと使いまわすことができます。

PDO を使ったアプリケーションじゃないと Database Extension を使えないの?

いいえ。PDO が必要なのは、フィクスチャの準備や後始末とアサーションのときだけです。 テスト対象のコード内では、なんでもお好みの方法でデータベースにアクセスできます。

Too much Connections というエラーが出たらどうすればいい?

テストケースの getConnection() メソッドで作った PDO インスタンスをキャッシュしていなければ、 データベースを使うテストを実行するたびにデータベースへの接続の数は増加し続けます。 デフォルトの設定では MySQL が受け付ける同時接続は 100 までであり、 他のデータベースにも同様の接続数制限があります。

自前でのデータベーステストケースの抽象化 に、このエラーを回避する方法を示しています。 ひとつの PDO インスタンスをキャッシュして、すべてのテストで使いまわす方法です。

フラット XML や CSV のデータセットで NULL を扱う方法は?

そんな方法はありません。NULL が使いたければ XML あるいは YAML データセットを使わないといけません。

第9章 テストダブル

Gerard Meszaros は、テストダブルの概念を [Meszaros2007] でこのように述べています。

 

Sometimes it is just plain hard to test the system under test (SUT) because it depends on other components that cannot be used in the test environment. This could be because they aren't available, they will not return the results needed for the test or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control or visibility of the internal behavior of the SUT.

- テスト対象のシステム (SUT: system under test) をテストすることは、時に非常に困難なこととなります。というのも、 システムが他のコンポーネントに依存しており、 そのコンポーネントをテスト環境で利用できないことがあるからです。 そもそも使用不可能であったりテストで必要な結果を返さなかったり、 あるいは好ましくない副作用があったりといったことです。 それ以外の場合も、テスト環境の内部的な振る舞いをきちんと制御して 目に見えるようにしておくことが必要です。

When we are writing a test in which we cannot (or chose not to) use a real depended-on component (DOC), we can replace it with a Test Double. The Test Double doesn't have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the SUT thinks it is the real one!

- 実際に依存するコンポーネント (DOC: depended-on component) を使わないテストを書く場合は、それをテストダブルで置き換えることができます。 テストダブルは、必ずしも実際の DOC とまったく同様に動作する必要はありません。 単に実際のものと同じ API を提供し、 SUT に「これは本物だ!」と思わせるだけでいいのです。

 
 --Gerard Meszaros

PHPUnit の createMock($type) メソッドや getMockBuilder($type) メソッドを使うと、 指定した元インターフェイス (あるいは元クラス) のテストダブルとして振る舞うオブジェクトを自動的に生成することができます。 このテストダブルオブジェクトは、元のオブジェクトを要するすべての場面で使うことができます。

createMock($type) メソッドは、指定した型 (インターフェイスやクラス) のテストダブルオブジェクトをその場で返します。 テストダブルの作成は、デフォルトではベストプラクティスに沿って行われます (元クラスの __construct()__clone() は実行せず、テストダブルのメソッドに渡された引数はクローンされません)。 デフォルトと異なる挙動を求める場合は、 getMockBuilder($type) メソッドを用いてテストダブルの生成処理をカスタマイズする必要があります。

デフォルトでは、元クラスのすべてのメソッドが置き換えられて、 (元のメソッドは呼び出さずに) 単に null を返すだけのダミー実装になります。たとえば will($this->returnValue()) メソッドを使うと、 ダミー実装がコールされたときに値を返すよう設定することができます。

制限:final、private および static メソッド

final, private および static メソッドのスタブやモックは作れないことに注意しましょう。 PHPUnit のテストダブル機能ではこれらを無視し、元のメソッドの振る舞いをそのまま維持します。

スタブ

実際のオブジェクトを置き換えて、 設定した何らかの値を (オプションで) 返すようなテストダブルのことを スタブ といいます。 スタブ を使うと、 「SUT が依存している実際のコンポーネントを置き換え、 SUT の入力を間接的にコントロールできるようにすることができます。 これにより、SUT が他の何者も実行しないことを強制させることができます。」

例 9.2 に、スタブメソッドの作成と返り値の設定の方法を示します。まず、 PHPUnit\Framework\TestCase クラスの createMock() メソッドを用いて SomeClass オブジェクトのスタブを作成します (例 9.1)。 次に、PHPUnit が提供する、いわゆる Fluent Interface (流れるようなインターフェイス) を用いてスタブの振る舞いを指定します。簡単に言うと、 いくつもの一時オブジェクトを作成して、 それらを連結するといった操作は必要ないということです。 そのかわりに、例にあるようにメソッドの呼び出しを連結します。 このほうが、より読みやすく "流れるような" コードとなります。

例 9.1: スタブを作りたいクラス

<?php
use PHPUnit\Framework\TestCase;

class SomeClass
{
    public function doSomething()
    {
        // なにかをします
    }
}
?>

例 9.2: メソッドに固定値を返させるスタブ

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->createMock(SomeClass::class);

        // スタブの設定を行います
        $stub->method('doSomething')
             ->willReturn('foo');

        // $stub->doSomething() をコールすると
        // 'foo' を返すようになります
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>

制限:"method" という名前のメソッド

この例がきちんと動作するのは、元のクラスで "method" という名前のメソッドが宣言されていない場合だけです。

元のクラスで "method" という名前のメソッドが宣言されている場合は、 $stub->expects($this->any())->method('doSomething')->willReturn('foo'); としなければいけません。

舞台裏では、createMock() メソッドが使われたときに PHPUnit が自動的に、求める振る舞いを実装した新たな PHP のクラスを生成しています。

例 9.3 に例を示します。 これは、モックビルダーの流れるようなインターフェイスを使って、テストダブルの作成方法を設定するものです。 このテストダブルで使っている設定は、createMock() がデフォルトで使用するベストプラクティスと同じです。

例 9.3: モックビルダー API を使った、生成されるテストダブルクラスの変更

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMockBuilder($originalClassName)
                     ->disableOriginalConstructor()
                     ->disableOriginalClone()
                     ->disableArgumentCloning()
                     ->disallowMockingUnknownTypes()
                     ->getMock();

        // スタブの設定を行います
        $stub->method('doSomething')
             ->willReturn('foo');

        // $stub->doSomething() をコールすると
        // 'foo' を返すようになります
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>

ここまでの例では、 willReturn($value) を使ってシンプルな値を返していました。 この構文は、 will($this->returnValue($value)) と同じ意味です。 この長い構文での検証を使うと、より複雑な動きをするスタブも作れます。

時には、メソッドをコールした際の引数のひとつを (そのまま) スタブメソッドコールの返り値としたいこともあるでしょう。 例 9.4 は、 returnValue() のかわりに returnArgument() を用いてこれを実現する例です。

例 9.4: メソッドに引数のひとつを返させるスタブ

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testReturnArgumentStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->createMock(SomeClass::class);

        // スタブの設定を行います
        $stub->method('doSomething')
             ->will($this->returnArgument(0));

        // $stub->doSomething('foo') は 'foo' を返します
        $this->assertEquals('foo', $stub->doSomething('foo'));

        // $stub->doSomething('bar') は 'bar' を返します
        $this->assertEquals('bar', $stub->doSomething('bar'));
    }
}
?>

流れるようなインターフェイスをテストするときには、 スタブメソッドがオブジェクト自身への参照を返すようにできると便利です。 例 9.5 は、 returnSelf() を使ってこれを実現する例です。

例 9.5: スタブオブジェクトへの参照を返すメソッドのスタブ

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testReturnSelf()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->createMock(SomeClass::class);

        // スタブの設定を行います
        $stub->method('doSomething')
             ->will($this->returnSelf());

        // $stub->doSomething() は $stub を返します
        $this->assertSame($stub, $stub->doSomething());
    }
}
?>

スタブメソッドをコールした結果として、 定義済みの引数リストにあわせて異なる値を返さなければならないこともあるでしょう。 returnValueMap() を使えば、 マップを作って引数と関連付け、それを返り値に対応させることができます。 例 9.6 を参照ください。

例 9.6: メソッドにマップからの値を返させるスタブ

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testReturnValueMapStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->createMock(SomeClass::class);

        // 値を返すための、引数のマップを作製します
        $map = [
            ['a', 'b', 'c', 'd'],
            ['e', 'f', 'g', 'h']
        ];

        // スタブの設定を行います
        $stub->method('doSomething')
             ->will($this->returnValueMap($map));

        // $stub->doSomething() は、渡した引数に応じて異なる値を返します
        $this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
        $this->assertEquals('h', $stub->doSomething('e', 'f', 'g'));
    }
}
?>

スタブメソッドをコールした結果として固定値 (returnValue() を参照ください) や (不変の) 引数 (returnArgument() を参照ください) ではなく計算した値を返したい場合は、 returnCallback() を使用します。 これは、スタブメソッドからコールバック関数やメソッドの結果を返させます。 例 9.7 を参照ください。

例 9.7: メソッドにコールバックからの値を返させるスタブ

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testReturnCallbackStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->createMock(SomeClass::class);

        // スタブの設定を行います
        $stub->method('doSomething')
             ->will($this->returnCallback('str_rot13'));

        // $stub->doSomething($argument) は str_rot13($argument) を返します
        $this->assertEquals('fbzrguvat', $stub->doSomething('something'));
    }
}
?>

コールバックメソッドを設定するよりももう少しシンプルな方法として、 希望する返り値のリストを指定することもできます。この場合に使うのは onConsecutiveCalls() メソッドです。 例 9.8 の例を参照ください。

例 9.8: メソッドに、リストで指定した値をその順で返させるスタブ

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testOnConsecutiveCallsStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->createMock(SomeClass::class);

        // スタブの設定を行います
        $stub->method('doSomething')
             ->will($this->onConsecutiveCalls(2, 3, 5, 7));

        // $stub->doSomething() は毎回異なる値を返します
        $this->assertEquals(2, $stub->doSomething());
        $this->assertEquals(3, $stub->doSomething());
        $this->assertEquals(5, $stub->doSomething());
    }
}
?>

値を返すのではなく、スタブメソッドで例外を発生させることもできます。 例 9.9 に、throwException() でこれを行う方法を示します。

例 9.9: メソッドに例外をスローさせるスタブ

<?php
use PHPUnit\Framework\TestCase;

class StubTest extends TestCase
{
    public function testThrowExceptionStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->createMock(SomeClass::class);

        // スタブの設定を行います
        $stub->method('doSomething')
             ->will($this->throwException(new Exception));

        // $stub->doSomething() は例外をスローします
        $stub->doSomething();
    }
}
?>

また、スタブを使用することで、よりよい設計を行うことができるようにもなります。 あちこちで使用されているリソースを単一の窓口 (façade : ファサード) 経由でアクセスするようにすることで、 それを簡単にスタブに置き換えられるようになります。例えば、 データベースへのアクセスのコードをそこらじゅうにちりばめるのではなく、 その代わりに IDatabase インターフェイスを実装した単一の Database オブジェクトを使用するようにします。すると、 IDatabase を実装したスタブを作成することで、 それをテストに使用できるようになるのです。同時に、 テストを行う際にスタブデータベースを使用するか 本物のデータベースを使用するかを選択できるようになります。 つまり開発時にはローカル環境でテストし、 統合テスト時には実際のデータベースでテストするといったことができるようになるのです。

スタブ化しなければならない機能は、たいてい同一オブジェクト内で密結合しています。 この機能ををひとつの結合したインターフェイスにまとめることで、 システムのそれ以外の部分との結合を緩やかにすることができます。

モックオブジェクト

実際のオブジェクトを置き換えて、 (メソッドがコールされたことなどの) 期待する内容を検証するテストダブルのことを モック といいます。

モックオブジェクト は "SUT の間接的な出力の内容を検証するために使用する観測地点です。 一般的に、モックオブジェクトにはテスト用スタブの機能も含まれます。 まだテストに失敗していない場合に、間接的な出力の検証用の値を SUT に返す機能です。 したがって、モックオブジェクトとは テスト用スタブにアサーション機能を足しただけのものとは異なります。 それ以外の用途にも使うことができます" (Gerard Meszaros)。

制限:期待値の自動検証

そのテストのスコープ内で生成されたモックオブジェクトだけが、PHPUnit による自動検証の対象となります。 たとえば、データプロバイダなどで生成されたモックオブジェクトや @depends アノテーションで注入されたオブジェクトについては、PHPUnit では検証しません。

ひとつ例を示します。ここでは、別のオブジェクトを観察している あるオブジェクトの特定のメソッド (この例では update()) が正しくコールされたかどうかを調べるものとします。 例 9.10 は、テスト対象のシステム (SUT) の一部である Subject クラスと Observer クラスのコードです。

例 9.10: テスト対象のシステム (SUT) の一部である Subject クラスと Observer クラス

<?php
use PHPUnit\Framework\TestCase;

class Subject
{
    protected $observers = [];
    protected $name;
    
    public function __construct($name)
    {
        $this->name = $name;
    }
    
    public function getName()
    {
        return $this->name;
    }

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function doSomething()
    {
        // なにかをします
        // ...

        // なにかしたということをオブザーバに通知します
        $this->notify('something');
    }

    public function doSomethingBad()
    {
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }

    protected function notify($argument)
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }

    // その他のメソッド
}

class Observer
{
    public function update($argument)
    {
        // なにかをします
    }

    public function reportError($errorCode, $errorMessage, Subject $subject)
    {
        // なにかをします
    }

    // その他のメソッド
}
?>

例 9.11 では、モックオブジェクトを作成して Subject オブジェクトと Observer オブジェクトの対話をテストする方法を説明します。

まず PHPUnit\Framework\TestCase クラスの getMockBuilder() メソッドを使用して Observer のモックオブジェクトを作成します。 getMock() メソッドの二番目の (オプションの) パラメータに配列を指定しているので、Observer クラスの中の update() メソッドについてのみモック実装が作成されます。

あるメソッドがコールされたのかどうか、そしてどんな引数を渡してコールされたのかを検証したいので、 expects() メソッドと with() メソッドを用意しました。 これらを使って、このやりとりがどのように行われるのかを指定します。

例 9.11: あるメソッドが、指定した引数で一度だけコールされることを確かめるテスト

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testObserversAreUpdated()
    {
        // Observer クラスのモックを作成します。
        // update() メソッドのみのモックです。
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['update'])
                         ->getMock();

        // update() メソッドが一度だけコールされ、その際の
        // パラメータは文字列 'something' となる、
        // ということを期待しています。
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));

        // Subject オブジェクトを作成し、Observer オブジェクトの
        // モックをアタッチします。
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // $subject オブジェクトの doSomething() メソッドをコールします。
        // これは、Observer オブジェクトのモックの update() メソッドを、
        // 文字列 'something' を引数としてコールすることを期待されています。
        $subject->doSomething();
    }
}
?>

with() メソッドには任意の数の引数を渡すことができます。 これは、モック対象のメソッドの引数の数に対応します。 メソッドの引数に対して、単なるマッチだけでなくより高度な制約を指定することもできます。

例 9.12: メソッドが引数つきでコールされることを、さまざまな制約の下でテストする例

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testErrorReported()
    {
        // Observer クラスのモックを作成します。
        // reportError() メソッドをモックします。
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['reportError'])
                         ->getMock();

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with(
                       $this->greaterThan(0),
                       $this->stringContains('Something'),
                       $this->anything()
                   );

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() メソッドは、
        // reportError() メソッドを通じてオブザーバにエラーを報告しなければなりません。
        $subject->doSomethingBad();
    }
}
?>

withConsecutive() メソッドには、 テスト対象の呼び出しにあわせて、引数の配列を好きなだけ渡せます。 個々の配列は制約のリストです。 with() と同様に、これがモック対象メソッドのそれぞれの引数に対応します。

例 9.13: あるメソッドが、指定した引数つきで 2 回呼び出されることを確かめるテスト

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testFunctionCalledTwoTimesWithSpecificArguments()
    {
        $mock = $this->getMockBuilder(stdClass::class)
                     ->setMethods(['set'])
                     ->getMock();

        $mock->expects($this->exactly(2))
             ->method('set')
             ->withConsecutive(
                 [$this->equalTo('foo'), $this->greaterThan(0)],
                 [$this->equalTo('bar'), $this->greaterThan(0)]
             );

        $mock->set('foo', 21);
        $mock->set('bar', 48);
    }
}
?>

callback() 制約を使えば、より複雑な引数の検証ができます。 この制約は、PHP のコールバックを引数として受け取ります。 このコールバックは、検証したい引数を受け取って、検証を通過した場合に true、 それ以外の場合に false を返します。

例 9.14: より複雑な引数の検証

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testErrorReported()
    {
        // Observer クラスのモックを作成します。
        // reportError() メソッドをモックします。
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['reportError'])
                         ->getMock();

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with($this->greaterThan(0),
                        $this->stringContains('Something'),
                        $this->callback(function($subject){
                          return is_callable([$subject, 'getName']) &&
                                 $subject->getName() == 'My subject';
                        }));

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() メソッドは、
        // reportError() メソッドを通じてオブザーバにエラーを報告しなければなりません。
        $subject->doSomethingBad();
    }
}
?>

例 9.15: メソッドが一度だけ呼ばれ、同じオブジェクトが渡されたことを確かめるテスト

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testIdenticalObjectPassed()
    {
        $expectedObject = new stdClass;

        $mock = $this->getMockBuilder(stdClass::class)
                     ->setMethods(['foo'])
                     ->getMock();

        $mock->expects($this->once())
             ->method('foo')
             ->with($this->identicalTo($expectedObject));

        $mock->foo($expectedObject);
    }
}
?>

例 9.16: パラメータのクローンの有効にしたモックオブジェクトの作成

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testIdenticalObjectPassed()
    {
        $cloneArguments = true;

        $mock = $this->getMockBuilder(stdClass::class)
                     ->enableArgumentCloning()
                     ->getMock();

        // これでモックがパラメータをクローンするようになり、
        // identicalTo 制約は失敗します
    }
}
?>

表 A.1 はメソッドの引数に適用できる制約、そして 表 9.1 は起動回数を指定するために使える matcher です。

表9.1 Matchers

Matcher意味
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any()評価対象のメソッドがゼロ回以上実行された際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedCount never()評価対象のメソッドが実行されなかった際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce()評価対象のメソッドが最低一回以上実行された際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedCount once()評価対象のメソッドが一度だけ実行された際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count)評価対象のメソッドが指定した回数だけ実行された際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index)評価対象のメソッドが $index 回目に実行された際にマッチするオブジェクトを返します。

注記

at() マッチャーのパラメータ $index は、 指定したモックオブジェクトでの すべてのメソッドの実行 の、ゼロからはじまるインデックスを参照します。 このマッチャーを使うときには注意しましょう。テストが実装の詳細とあまりにも密結合になり、 脆いテストになってしまう可能性があるからです。

最初に説明したとおり、createMock() メソッドが用いるデフォルトのテストダブル生成方法がニーズを満たさない場合は、 getMockBuilder($type) メソッドを使えば生成方法をカスタマイズできます。 モックビルダーが提供するメソッドの一覧は、次のとおりです。

  • setMethods(array $methods) をモックビルダーオブジェクト上でコールすると、テストダブルで置き換えるメソッドを指定することができます。その他のメソッドの挙動は変更しません。setMethods(NULL) とすると、どのメソッドも置き換えません。

  • setConstructorArgs(array $args) をコールしてパラメータの配列を渡すと、それを元クラスのコンストラクタに渡すことができます (デフォルトのダミー実装では、コンストラクタは置き換えません)。

  • setMockClassName($name) を使うと、生成されるテストダブルクラスのクラス名を指定することができます。

  • disableOriginalConstructor() を使うと、元クラスのコンストラクタを無効にすることができます。

  • disableOriginalClone() を使うと、元クラスのクローンコンストラクタを無効にすることができます。

  • disableAutoload() を使うと、テストダブルクラスを生成するときに __autoload() を無効にすることができます。

Prophecy

Prophecy は 「クセは強いけれども、強力で柔軟な、PHP のオブジェクトモッキングフレームワークです。 最初は phpspec2 のニーズを満たすために作られましたが、今やそれ以外のテスティングフレームワークでも、 最小限の努力で使えるようになりました」 とのことです。

PHPUnit は、Prophecy を使ったテストダブルの作成に標準で対応しています。 例 9.17 は、例 9.11 と同じテストを、Prophecy の理念に沿って表すとどうなるかを示す例です。

例 9.17: あるメソッドが、指定した引数で一度だけコールされることを確かめるテスト

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testObserversAreUpdated()
    {
        $subject = new Subject('My subject');

        // Observer クラスの prophecy を作成します。
        $observer = $this->prophesize(Observer::class);

        // update() メソッドが一度だけコールされ、その際の
        // パラメータは文字列 'something' となる、
        // ということを期待しています。
        $observer->update('something')->shouldBeCalled();

        // prophecy を公開し、モックオブジェクトを
        // Subject にアタッチします。
        $subject->attach($observer->reveal());

        // $subject オブジェクトの doSomething() メソッドをコールします。
        // これは、Observer オブジェクトのモックの update() メソッドを、
        // 文字列 'something' を引数としてコールすることを期待されています。
        $subject->doSomething();
    }
}
?>

Prophecy を使ってスタブやスパイそしてモックを作ったり設定したり使ったりする方法の詳細については、 その ドキュメント を参照ください。

トレイトと抽象クラスのモック

getMockForTrait() メソッドは、指定したトレイトを使ったモックオブジェクトを返します。 そのトレイトのすべての抽象メソッドがモックの対象となります。 これを使えば、トレイトの具象メソッドをテストすることができます。

例 9.18: トレイトの具象メソッドのテスト

<?php
use PHPUnit\Framework\TestCase;

trait AbstractTrait
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class TraitClassTest extends TestCase
{
    public function testConcreteMethod()
    {
        $mock = $this->getMockForTrait(AbstractTrait::class);

        $mock->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(true));

        $this->assertTrue($mock->concreteMethod());
    }
}
?>

getMockForAbstractClass() メソッドは、 抽象クラスのモックオブジェクトを返します。 そのクラスのすべての抽象メソッドがモックの対象となります。 これを使えば、抽象クラスにある具象メソッドをテストすることができます。

例 9.19: 抽象クラスの具象メソッドのテスト

<?php
use PHPUnit\Framework\TestCase;

abstract class AbstractClass
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class AbstractClassTest extends TestCase
{
    public function testConcreteMethod()
    {
        $stub = $this->getMockForAbstractClass(AbstractClass::class);

        $stub->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(true));

        $this->assertTrue($stub->concreteMethod());
    }
}
?>

ウェブサービスのスタブおよびモック

ウェブサービスとのやりとりを行うアプリケーションを、 実際にウェブサービスとやりとりすることなくテストしたくなることもあるでしょう。 ウェブサービスのスタブやモックを作りやすくするために getMockFromWsdl() メソッドが用意されており、これは getMock() (上を参照ください) とほぼ同様に使うことができます。唯一の違いは、 getMockFromWsdl() が返すスタブやモックが WSDL のウェブサービス記述にもとづくものであるのに対して getMock() が返すスタブやモックが PHP のクラスやインターフェイスにもとづくものであるという点です。

例 9.20 は、getMockFromWsdl() を使って GoogleSearch.wsdl に記述されたウェブサービスのスタブを作る例です。

例 9.20: ウェブサービスのスタブ

<?php
use PHPUnit\Framework\TestCase;

class GoogleTest extends TestCase
{
    public function testSearch()
    {
        $googleSearch = $this->getMockFromWsdl(
          'GoogleSearch.wsdl', 'GoogleSearch'
        );

        $directoryCategory = new stdClass;
        $directoryCategory->fullViewableName = '';
        $directoryCategory->specialEncoding = '';

        $element = new stdClass;
        $element->summary = '';
        $element->URL = 'https://phpunit.de/';
        $element->snippet = '...';
        $element->title = '<b>PHPUnit</b>';
        $element->cachedSize = '11k';
        $element->relatedInformationPresent = true;
        $element->hostName = 'phpunit.de';
        $element->directoryCategory = $directoryCategory;
        $element->directoryTitle = '';

        $result = new stdClass;
        $result->documentFiltering = false;
        $result->searchComments = '';
        $result->estimatedTotalResultsCount = 3.9000;
        $result->estimateIsExact = false;
        $result->resultElements = [$element];
        $result->searchQuery = 'PHPUnit';
        $result->startIndex = 1;
        $result->endIndex = 1;
        $result->searchTips = '';
        $result->directoryCategories = [];
        $result->searchTime = 0.248822;

        $googleSearch->expects($this->any())
                     ->method('doGoogleSearch')
                     ->will($this->returnValue($result));

        /**
         * $googleSearch->doGoogleSearch() はスタブが用意した結果を返し、
         * ウェブサービスの doGoogleSearch() が呼び出されることはありません
         */
        $this->assertEquals(
          $result,
          $googleSearch->doGoogleSearch(
            '00000000000000000000000000000000',
            'PHPUnit',
            0,
            1,
            false,
            '',
            false,
            '',
            '',
            ''
          )
        );
    }
}
?>

ファイルシステムのモック

vfsStream仮想ファイルシステム 用の ストリームラッパー で、 ユニットテストにおいて実際のファイルシステムのモックを作るときに有用です。

Composer を使ってプロジェクトの依存関係を管理するには、 mikey179/vfsStream への依存情報をプロジェクトの composer.json ファイルに追加します。 次に示すのは最小限の composer.json ファイルの例で、 開発時の PHPUnit 4.6 と vfsStream への依存を定義しています。

{
    "require-dev": {
        "phpunit/phpunit": "~4.6",
        "mikey179/vfsStream": "~1"
    }
}

例 9.21 は、ファイルシステムを操作するクラスの例です。

例 9.21: ファイルシステムを操作するクラス

<?php
use PHPUnit\Framework\TestCase;

class Example
{
    protected $id;
    protected $directory;

    public function __construct($id)
    {
        $this->id = $id;
    }

    public function setDirectory($directory)
    {
        $this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;

        if (!file_exists($this->directory)) {
            mkdir($this->directory, 0700, true);
        }
    }
}?>

vfsStream のような仮想ファイルシステムがなければ、外部への影響なしに setDirectory() メソッドを個別にテストすることができません (例 9.22 を参照ください)。

例 9.22: ファイルシステムを操作するクラスのテスト

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    protected function setUp()
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(file_exists(dirname(__FILE__) . '/id'));

        $example->setDirectory(dirname(__FILE__));
        $this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
    }

    protected function tearDown()
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }
}
?>

この方式には、次のような問題があります。

  • 外部のリソースを使うため、ファイルシステムのテストが断続的になる可能性があります。その結果、テストがあまり当てにならないものになります。

  • setUp()tearDown() で、テストの前後にそのディレクトリがないことを確認する必要があります。

  • tearDown() メソッドを実行する前にテストが異常終了したときに、ファイルシステム上にディレクトリが残ったままとなります。

例 9.23 は、vfsStream を使ってファイルシステムのモックを作成し、 ファイルシステムを操作するクラスのテストを行う例です。

例 9.23: ファイルシステムを操作するクラスのテストにおけるファイルシステムのモックの作成

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    public function setUp()
    {
        vfsStreamWrapper::register();
        vfsStreamWrapper::setRoot(new vfsStreamDirectory('exampleDir'));
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id'));

        $example->setDirectory(vfsStream::url('exampleDir'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id'));
    }
}
?>

この方式には次のような利点があります。

  • テストが簡潔になります。

  • vfsStream が、テスト対象のコードから操作するファイルシステム環境を用意してくれるので、開発者はそれを自由に扱えるようになります。

  • 実際のファイルシステムを操作することがなくなるので、tearDown() メソッドでの後始末が不要になります。

第10章 テストの進め方

 

You can always write more tests. However, you will quickly find that only a fraction of the tests you can imagine are actually useful. What you want is to write tests that fail even though you think they should work, or tests that succeed even though you think they should fail. Another way to think of it is in cost/benefit terms. You want to write tests that will pay you back with information.

テストはいくらでも書くことができる。でも、じきにわかるだろうが、 きみが考えているテストの中で本当に有用なものはごくわずかだ。 本当に書かなきゃいけないのは、 これは動くだろうと考えているにもかかわらず失敗するテスト。それから、 これは失敗するだろうと考えているにもかかわらず実際は成功するテストだ。 あるいはコストと利益の観点から考えてみてもいいだろう。 きみに何らかの情報を返してくれるテストを書かないとね。

 
 --Erich Gamma

開発中のテスト

開発中のソフトウェアの内部構造を変更し、 わかりやすく変更が簡単なものにする必要が出てきたときのことを考えましょう。 それによってソフトウェアの外部的な振る舞いが変わってしまってはいけません。 この、いわゆる リファクタリング (日本語) を安全に行うにあたり、テストスイートが非常に重要となります。 もしテストスイートがなければ、リファクタリングによってシステムを壊してしまっても あなたはそれに気づかないでしょう。

以下の条件が、あなたのプロジェクトのコードや設計を改善するための助けとなるでしょう。 また、ユニットテストを使用することで、リファクタリングによって振る舞いが変化していないこと・ エラーが発生していないことが確認できます。

  1. すべてのユニットテストが正常に動作すること。

  2. コードが設計指針を満たしていること。

  3. コードに冗長性がないこと。

  4. コードには最小限のクラスおよびメソッドのみが含まれていること。

システムに新しい機能を追加する際には、まず最初にテストを書きます。 そのテストがきちんと実行できるようになった時点で、開発は終了です。 この手法については、次の章で詳しく説明します。

デバッグ中のテスト

不具合の報告を受けたら、すぐにでもそれを修正したいと思われることでしょう。 しかし、あせって修正しようとしても、経験上なかなかうまくいきません。 不具合を修正したつもりが新たな不具合を引き起こしていたなんてこともありがちですね。

はやる気持ちを抑えて、以下のようにしてみましょう。

  1. 不具合を再現できることを確認します。

  2. 不具合が発生する最小限のコードを見つけます。例えば、 もしおかしな数値が出力されるのなら、 その数値を計算しているオブジェクトが何なのかを探します。

  3. その不具合のせいで今は失敗する (そして、不具合が修正されたら成功する) テストを書きます。

  4. 不具合を修正します。

不具合が再現する最小限のコードを見つける過程で、 不具合の原因がわかるかもしれません。テストを書くことによって、 不具合を真の意味で修正できる可能性が高まるでしょう。なぜなら、 テストを書くことで、将来同じ間違いをする可能性を減らせるからです。 これまでに書いたすべてのテストが、 不注意によって別の問題を発生させる可能性を減らすために役立っているのです。

 

Unit testing offers many advantages:

  • Testing gives code authors and reviewers confidence that patches produce the correct results.

  • Authoring testcases is a good impetus for developers to discover edge cases.

  • Testing provides a good way to catch regressions quickly, and to make sure that no regression will be repeated twice.

  • Unit tests provide working examples for how to use an API and can significantly aid documentation efforts.

Overall, integrated unit testing makes the cost and risk of any individual change smaller. It will allow the project to make [...] major architectural improvements [...] quickly and confidently.

ユニットテストには、こんなに多くの利点がある。

  • 実際にコードを書いた人とコードをレビューする人が意識を共有できるようになり、パッチの精度があがる。

  • テストケースを書くことは、いろいろな想定外の事態に備えるための原動力となる。

  • テストにより、リグレッションを早期に発見できるようになる。また、同じリグレッションを二度と起こさないようになる。

  • ユニットテストそのものが、その API の使用法についてのよいサンプルとなる。ドキュメント作成の際に助けとなるだろう。

まとめよう。ユニットテストをうまく組み込めば、 プログラムを変更する際の手間やリスクをより減らすことになるのだ。 プロジェクトが【中略】のアーキテクチャに関する大きな改修【中略】を素早く、自信を持って行うことを可能にするだろう。

 
 --Benjamin Smedberg

第11章 コードカバレッジ解析

 

In computer science, code coverage is a measure used to describe the degree to which the source code of a program is tested by a particular test suite. A program with high code coverage has been more thoroughly tested and has a lower chance of containing software bugs than a program with low code coverage.

 
 --Wikipedia

この章では、PHPUnit のコードカバレッジ機能について学びます。 これは、テストを実行したときに、実装コードのどの部分が実行されたかを調べるものです。 PHPUnit のコードカバレッジ解析では PHP_CodeCoverage コンポーネントを使っています。このコンポーネントは、 Xdebug 拡張モジュールが提供するステートメントカバレッジ機能を利用しています。

注記

Xdebug は PHPUnit 本体には組み込まれていません。 テストを実行したときに Xdebug がロードできないという notice が出る場合は、 Xdebug がインストールされていないかあるいはうまく設定できていないのでしょう。 PHPUnit のコードカバレッジ機能を使う前に、まずは Xdebug のインストールガイド を読んでみましょう。

PHPUnit は、HTML ベースのコードカバレッジレポートを生成するだけでなく、 XML ベースのログファイルにコードカバレッジ情報を出力することもできます。 Clover、Crap4J、PHPUnit など、さまざまな形式に対応しています。 また、コードカバレッジ情報をテキスト形式で出力 (そして、標準出力に表示) したり、PHP のコードとして出力して後処理をしたりすることもできます。

コードカバレッジ機能を制御するための コマンドラインスイッチの一覧は、第 3 章 を参照ください。 また、設定項目については 「ログ出力」 を参照ください。

コードカバレッジの指標

コードカバレッジを計測するための指標には、さまざまなものがあります。

Line Coverage

ラインカバレッジ は、 実行可能な行が実行されたかどうかを計測します。

Function and Method Coverage

関数・メソッドカバレッジ は、 関数やメソッドが実行されたかどうかを計測します。 PHP_CodeCoverage は、その関数やメソッド内の実行可能な行がすべて実行された場合にのみ、 その関数やメソッドが実行されたとみなします。

クラス・トレイトカバレッジ

クラス・トレイトカバレッジ は、 クラスやトレイトがカバーされたかどうかを計測します。 PHP_CodeCoverage は、クラスやトレイト内のすべてのメソッドがカバーされている場合にのみ、 そのクラスやトレイトがカバーされたとみなします。

Opcode Coverage

オペコードカバレッジ は、関数やメソッドのオペコードが、 テストスイートの実行中に実行されたかどうかを計測します。 通常は、1 行のコードをコンパイルすると、複数のオペコードになります。 ラインカバレッジは、複数のオペコードのうち少なくともひとつが実行された時点で、 その行が実行されたとみなします。

Branch Coverage

ブランチカバレッジ は、テストスイートの実行中に、 制御構造内の boolean 式が true あるいは false のどちらかとして評価されたかどうかを計測します。

Path Coverage

パスカバレッジ は、テストスイートの実行中に、 関数やメソッド内で取りうる実行パスが網羅されたかどうかを計測します。 実行パスとは、関数やメソッドに入ってから出るまでの間のルート内での分岐のことです。

Change Risk Anti-Patterns (CRAP) Index

Change Risk Anti-Patterns (CRAP) インデックス とは、循環的複雑度と、あるコード単位のコードカバレッジに基づいて算出される指標です。 複雑度が低く、適切なテストカバレッジが達成されているコードは、CRAPインデックスの値が低くなります。 CRAPインデックスを下げるには、テストを書くか、 あるいはリファクタリングでコードの複雑性を下げます。

注記

オペコードカバレッジブランチカバレッジパスカバレッジ については、 PHP_CodeCoverage ではまだサポートしていません。

ファイルのホワイトリスト

ホワイトリスト を設定して、 PHPUnit に対してどのソースコードファイルをコードカバレッジレポートに含めるかを指定する必要があります。 ホワイトリストの設定には、コマンドラインオプションの --whitelist を使うか、あるいは設定ファイルを使います (「コードカバレッジ対象のファイルのホワイトリスト」 を参照ください)。

オプションで、ホワイトリストに追加したファイルをすべて、コードカバレッジレポートに追加することもできます。 そのためには、PHPUnit の設定で addUncoveredFilesFromWhitelist="true" とします (「コードカバレッジ対象のファイルのホワイトリスト」 を参照ください)。 こうすれば、まだテストされていないファイルもすべて、レポートに含めることができます。 カバーされていないファイルにおける、実行可能な行についての情報を知りたい場合は、同じく PHPUnit の設定で processUncoveredFilesFromWhitelist="true" とします (「コードカバレッジ対象のファイルのホワイトリスト」 を参照ください)。

注記

processUncoveredFilesFromWhitelist="true" が設定されている場合のソースコードファイルの読み込みでは、 もしクラスや関数のスコープから外れるコードが含まれていたときに問題が起こる可能性があります。

コードブロックの無視

どうしてもテストができないコードブロックなどを、 コードカバレッジ解析時に無視させたいこともあるでしょう。 PHPUnit でこれを実現するには、 @codeCoverageIgnore@codeCoverageIgnoreStart および @codeCoverageIgnoreEnd アノテーションを 例 11.1 のように使用します。

例 11.1: @codeCoverageIgnore@codeCoverageIgnoreStart および @codeCoverageIgnoreEnd アノテーションの使用法

<?php
use PHPUnit\Framework\TestCase;

/**
 * @codeCoverageIgnore
 */
class Foo
{
    public function bar()
    {
    }
}

class Bar
{
    /**
     * @codeCoverageIgnore
     */
    public function foo()
    {
    }
}

if (false) {
    // @codeCoverageIgnoreStart
    print '*';
    // @codeCoverageIgnoreEnd
}

exit; // @codeCoverageIgnore
?>

これらのアノテーションを使って無視するよう指定された行は、 もし実行可能なら (たとえ実行されていなくても) 実行されたものとみなされ、 強調表示されません。

カバーするメソッドの指定

テストコードで @covers アノテーション (表 B.1) を参照ください) を使用すると、 そのテストメソッドがどのメソッドをテストしたいのかを指定することができます。 これを指定すると、指定したメソッドのコードカバレッジ情報のみを考慮します。 例 11.2 に例を示します。

例 11.2: どのメソッドを対象とするかを指定したテスト

<?php
use PHPUnit\Framework\TestCase;

class BankAccountTest extends TestCase
{
    protected $ba;

    protected function setUp()
    {
        $this->ba = new BankAccount;
    }

    /**
     * @covers BankAccount::getBalance
     */
    public function testBalanceIsInitiallyZero()
    {
        $this->assertEquals(0, $this->ba->getBalance());
    }

    /**
     * @covers BankAccount::withdrawMoney
     */
    public function testBalanceCannotBecomeNegative()
    {
        try {
            $this->ba->withdrawMoney(1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::depositMoney
     */
    public function testBalanceCannotBecomeNegative2()
    {
        try {
            $this->ba->depositMoney(-1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::getBalance
     * @covers BankAccount::depositMoney
     * @covers BankAccount::withdrawMoney
     */
    public function testDepositWithdrawMoney()
    {
        $this->assertEquals(0, $this->ba->getBalance());
        $this->ba->depositMoney(1);
        $this->assertEquals(1, $this->ba->getBalance());
        $this->ba->withdrawMoney(1);
        $this->assertEquals(0, $this->ba->getBalance());
    }
}
?>

あるテストが、一切メソッドをカバーしてはならないことも指定できます。 そのために使うのが @coversNothing アノテーションです。 (「@coversNothing」 を参照ください)。 これは、インテグレーションテストを書く際に ユニットテストだけのコードカバレッジを生成させたい場合に便利です。

例 11.3: どのメソッドもカバーすべきでないことを指定したテスト

<?php
use PHPUnit\Framework\TestCase;

class GuestbookIntegrationTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @coversNothing
     */
    public function testAddEntry()
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );

        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");

        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>
      

エッジケース

この節では、コードカバレッジ情報がわかりにくくなってしまうような、 エッジケースについて紹介します。

例 11.4:

<?php
use PHPUnit\Framework\TestCase;

// カバレッジは「行単位」であって文単位ではないので、
// 一行にまとめられた行はひとつのカバレッジ状態しか持ちません
if (false) this_function_call_shows_up_as_covered();

// コードカバレッジの内部動作上、これら 2 行は特別です。
// 次の行は「実行されていない」となります
if (false)
    // 次の行は「実行されている」となります
    // 実際のところ、ひとつ上の if 文のカバレッジ情報がここに表示されることになるからです!
    will_also_show_up_as_covered();

// これを避けるには、必ず波括弧を使わなければなりません
if (false) {
    this_call_will_never_show_up_as_covered();
}
?>

第12章 テストのその他の使用法

自動テストに慣れてくると、 ほかの目的のためにもテストを使いたくなってくることでしょう。 ここではそんな例を説明します。

アジャイルな文書作成

一般的に、エクストリームプログラミングのようなアジャイルプロセスを採用しているプロジェクトでは、 ドキュメントの内容が実際の設計やコードに追いついていないことが多いものです。 エクストリームプログラミングでは コードの共同所有 (collective code ownership) を要求しており、 すべての開発者がシステム全体の動作を知っておく必要があります。 作成するテストに対して、そのクラスが何を行うべきなのかを示すような 「わかりやすい」名前をつけられるようにさえしておけば、PHPUnit の TestDox 機能を使用して自動的にドキュメントを生成することができます。 このドキュメントにより、開発者たちはプロジェクト内の各クラスが どのようにふるまうべきなのかを知ることができます。

PHPUnit の TestDox 機能は、テストクラス内のすべてのテストメソッドの名前を抽出し、 それを PHP 風のキャメルケースから通常の文に変換します。つまり testBalanceIsInitiallyZero() が "Balance is initially zero" のようになるわけです。最後のほうの数字のみが違うメソッド、例えば testBalanceCannotBecomeNegative()testBalanceCannotBecomeNegative2() のようなものが存在した場合は、 文 "Balance cannot become negative" は一度のみ表示され、 全てのテストが成功したことを表します。

BankAccount クラスのアジャイルな文書を見てみましょう。

phpunit --testdox BankAccountTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

BankAccount
 [x] Balance is initially zero
 [x] Balance cannot become negative

また、アジャイルな文書を HTML あるいはプレーンテキスト形式で作成してファイルに書き出すこともできます。 この場合は、引数 --testdox-html あるいは --testdox-text を使用します。

アジャイルな文書は、プロジェクト内であなたが作成しようとしている外部パッケージについて、 このように動作するであるという期待をまとめた文書にもなります。 外部のパッケージを使用するときには、 そのパッケージが期待通りに動作しなくなるというリスクに常にさらされています。 パッケージのバージョンアップにより知らないうちに挙動が変わってしまい、 あなたのコードが動作しなくなる可能性もあります。そのようなことを避けるため、 「このパッケージはこのように動作するはず」 ということを常にテストケースで記述しておくようにします。テストが成功すれば、 期待通りに動作していることがわかります。もし動作仕様をすべてテストで記述できているのなら、 外部パッケージが将来バージョンアップされたとしても何の心配もいりません。 テストをクリアしたということは、システムは期待通りに動作するということだからです。

複数チームでのテスト

あるパッケージについての機能を文書化するためにテストを書いているとき、 そのテストの所有者はあなたです。今あなたがテストを作成しているパッケージの作者は、 そのテストのことについては何も知りません。パッケージの作者とよりつながりを深めるため、 作成したテストを使用してコミュニケートしたり、 そのテストを使用して共同作業をしたりすることができるでしょう。

あなたが作成したテストを使用してパッケージの作者と共同作業をすることになれば、 テストも共同で書くことになります。そうすることで、 より多くのテストケースを挙げられるようになるでしょう。 「暗黙の了解」などに頼っていては、共同作業はできません。 テストと同時に、あなたはそのパッケージに対して期待していることを正確に文書化することになります。 また、すべてのテストにクリアした時点で、 作者はパッケージが完成したことを知ることになります。

スタブ (本書の前のほうで説明した "モックオブジェクト" の章を参照ください) を使用することで、パッケージの作者と別れても作業できるようになります。 パッケージ作者の仕事は、パッケージの実際の実装でテストをクリアするようにすること。 そしてあなたの仕事はあなたが書いたコードでテストをクリアするようにすることです。 この段階になれば、あなたはスタブオブジェクトを使用すればよいのです。 このやり方により、2 つのチームが独立して開発できるようになります。

第13章 ログ出力

PHPUnit は、いくつかの形式のログファイルを作成することができます。

テスト結果 (XML)

PHPUnit が作成するテスト結果の XML のログファイルは、 Apache Ant の JUnit タスク が使用しているものを参考にしています。 以下の例は、ArrayTest のテストが生成した XML ログファイルです。

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="ArrayTest"
             file="/home/sb/ArrayTest.php"
             tests="2"
             assertions="2"
             failures="0"
             errors="0"
             time="0.016030">
    <testcase name="testNewArrayIsEmpty"
              class="ArrayTest"
              file="/home/sb/ArrayTest.php"
              line="6"
              assertions="1"
              time="0.008044"/>
    <testcase name="testArrayContainsAnElement"
              class="ArrayTest"
              file="/home/sb/ArrayTest.php"
              line="15"
              assertions="1"
              time="0.007986"/>
  </testsuite>
</testsuites>

次の XML ログファイルは、テストクラス FailureErrorTest にある 2 つのテスト testFailure および testError が出力したものです。失敗やエラーがどのように表示されるのかを確認しましょう。

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
  <testsuite name="FailureErrorTest"
             file="/home/sb/FailureErrorTest.php"
             tests="2"
             assertions="1"
             failures="1"
             errors="1"
             time="0.019744">
    <testcase name="testFailure"
              class="FailureErrorTest"
              file="/home/sb/FailureErrorTest.php"
              line="6"
              assertions="1"
              time="0.011456">
      <failure type="PHPUnit_Framework_ExpectationFailedException">
testFailure(FailureErrorTest)
Failed asserting that &lt;integer:2&gt; matches expected value &lt;integer:1&gt;.

/home/sb/FailureErrorTest.php:8
</failure>
    </testcase>
    <testcase name="testError"
              class="FailureErrorTest"
              file="/home/sb/FailureErrorTest.php"
              line="11"
              assertions="0"
              time="0.008288">
      <error type="Exception">testError(FailureErrorTest)
Exception:

/home/sb/FailureErrorTest.php:13
</error>
    </testcase>
  </testsuite>
</testsuites>

テスト結果 (TAP)

Test Anything Protocol (TAP) は、Perl のモジュールをテストする際に使用する、 シンプルなテキストベースのインターフェイスです。 以下の例は、ArrayTest のテストが生成した TAP ログファイルです。

TAP version 13
ok 1 - testNewArrayIsEmpty(ArrayTest)
ok 2 - testArrayContainsAnElement(ArrayTest)
1..2

次の TAP ログファイルは、テストクラス FailureErrorTest にあるメソッド testFailure および testError が出力したものです。失敗やエラーがどのように表示されるのかを確認しましょう。

TAP version 13
not ok 1 - Failure: testFailure(FailureErrorTest)
  ---
  message: 'Failed asserting that <integer:2> matches expected value <integer:1>.'
  severity: fail
  data:
    got: 2
    expected: 1
  ...
not ok 2 - Error: testError(FailureErrorTest)
1..2

テスト結果 (JSON)

JavaScript Object Notation (JSON) は、軽量なデータ交換用フォーマットです。次の例は、 ArrayTest のテストが作成した JSON メッセージです。

{"event":"suiteStart","suite":"ArrayTest","tests":2}
{"event":"test","suite":"ArrayTest",
 "test":"testNewArrayIsEmpty(ArrayTest)","status":"pass",
 "time":0.000460147858,"trace":[],"message":""}
{"event":"test","suite":"ArrayTest",
 "test":"testArrayContainsAnElement(ArrayTest)","status":"pass",
 "time":0.000422954559,"trace":[],"message":""}

次の JSON メッセージは、 FailureErrorTest にある 2 つのテスト testFailure および testError が出力したものです。失敗やエラーがどのように表示されるのかを確認しましょう。

{"event":"suiteStart","suite":"FailureErrorTest","tests":2}
{"event":"test","suite":"FailureErrorTest",
 "test":"testFailure(FailureErrorTest)","status":"fail",
 "time":0.0082459449768066,"trace":[],
 "message":"Failed asserting that <integer:2> is equal to <integer:1>."}
{"event":"test","suite":"FailureErrorTest",
 "test":"testError(FailureErrorTest)","status":"error",
 "time":0.0083680152893066,"trace":[],"message":""}

コードカバレッジ (XML)

PHPUnit がコードカバレッジ情報のログ出力の際に使用している XML のフォーマットは、 Clover のものを参考にしています。 以下の例は、BankAccountTest のテストが生成した XML ログファイルです。

<?xml version="1.0" encoding="UTF-8"?>
<coverage generated="1184835473" phpunit="4.8.0">
  <project name="BankAccountTest" timestamp="1184835473">
    <file name="/home/sb/BankAccount.php">
      <class name="BankAccountException">
        <metrics methods="0" coveredmethods="0" statements="0"
                 coveredstatements="0" elements="0" coveredelements="0"/>
      </class>
      <class name="BankAccount">
        <metrics methods="4" coveredmethods="4" statements="13"
                 coveredstatements="5" elements="17" coveredelements="9"/>
      </class>
      <line num="77" type="method" count="3"/>
      <line num="79" type="stmt" count="3"/>
      <line num="89" type="method" count="2"/>
      <line num="91" type="stmt" count="2"/>
      <line num="92" type="stmt" count="0"/>
      <line num="93" type="stmt" count="0"/>
      <line num="94" type="stmt" count="2"/>
      <line num="96" type="stmt" count="0"/>
      <line num="105" type="method" count="1"/>
      <line num="107" type="stmt" count="1"/>
      <line num="109" type="stmt" count="0"/>
      <line num="119" type="method" count="1"/>
      <line num="121" type="stmt" count="1"/>
      <line num="123" type="stmt" count="0"/>
      <metrics loc="126" ncloc="37" classes="2" methods="4" coveredmethods="4"
               statements="13" coveredstatements="5" elements="17"
               coveredelements="9"/>
    </file>
    <metrics files="1" loc="126" ncloc="37" classes="2" methods="4"
             coveredmethods="4" statements="13" coveredstatements="5"
             elements="17" coveredelements="9"/>
  </project>
</coverage>

コードカバレッジ (テキスト)

人間が読める形式のコードカバレッジ情報を、コマンドラインあるいはテキストファイルに出力します。 この出力フォーマットの狙いは、ちょっとしたクラス群のカバレッジの概要を手軽に把握することです。 大規模なプロジェクトでは、このフォーマットを使えばプロジェクト全体のカバレッジを大まかに把握しやすくなるでしょう。 --filter と組み合わせて使うこともできます。 コマンドラインから使う場合は php://stdout に書き込みます。 この出力は --colors の設定を反映したものになります。 コマンドラインから使った場合は、デフォルトの出力先は標準出力となります。 デフォルトでは、テストで少なくとも一行はカバーしているファイルしか表示しません。 この設定は、xml の showUncoveredFiles オプションでしか変更できません。 「ログ出力」 を参照ください。 デフォルトでは、すべてのファイルとそのカバレッジ情報が、詳細形式で表示されます。 この設定は、xml のオプション showOnlySummary で変更できます。

第14章 PHPUnit の拡張

テストを書きやすくする、あるいはテストの実行結果の表示方法を変更するなど、 PHPUnit はさまざまな方法で拡張することができます。 PHPUnit を拡張するための第一歩をここで説明します。

PHPUnit\Framework\TestCase のサブクラスの作成

PHPUnit\Framework\TestCase を継承した抽象サブクラスにカスタムアサーションやユーティリティメソッドを書き、 そのクラスをさらに継承してテストクラスを作成します。 これが、PHPUnit を拡張するための一番簡単な方法です。

カスタムアサーションの作成

カスタムアサーションを作成するときには、PHPUnit 自体のアサーションの実装方法を真似るのがおすすめです。 例 14.1 を見ればわかるとおり、 assertTrue() メソッドは isTrue() および assertThat() メソッドの単なるラッパーに過ぎません。 isTrue() が matcher オブジェクトを作り、それを assertThat() に渡して評価しています。

例 14.1: PHPUnit_Framework_Assert クラスの assertTrue() および isTrue() メソッド

<?php
use PHPUnit\Framework\TestCase;

abstract class PHPUnit_Framework_Assert
{
    // ...

    /**
     * Asserts that a condition is true.
     *
     * @param  boolean $condition
     * @param  string  $message
     * @throws PHPUnit_Framework_AssertionFailedError
     */
    public static function assertTrue($condition, $message = '')
    {
        self::assertThat($condition, self::isTrue(), $message);
    }

    // ...

    /**
     * Returns a PHPUnit_Framework_Constraint_IsTrue matcher object.
     *
     * @return PHPUnit_Framework_Constraint_IsTrue
     * @since  Method available since Release 3.3.0
     */
    public static function isTrue()
    {
        return new PHPUnit_Framework_Constraint_IsTrue;
    }

    // ...
}?>

例 14.2 は、 PHPUnit_Framework_Constraint_IsTrue が matcher オブジェクト (あるいは制約) のために抽象クラス PHPUnit_Framework_Constraint を継承している部分です。

例 14.2: PHPUnit_Framework_Constraint_IsTrue クラス

<?php
use PHPUnit\Framework\TestCase;

class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint
{
    /**
     * Evaluates the constraint for parameter $other. Returns true if the
     * constraint is met, false otherwise.
     *
     * @param mixed $other Value or object to evaluate.
     * @return bool
     */
    public function matches($other)
    {
        return $other === true;
    }

    /**
     * Returns a string representation of the constraint.
     *
     * @return string
     */
    public function toString()
    {
        return 'is true';
    }
}?>

assertTrue()isTrue() メソッドの実装を PHPUnit_Framework_Constraint_IsTrue クラスと同じようにしておけば、 アサーションの評価やタスクの記録 (テストの統計情報に自動的に更新するなど) を assertThat() が自動的に行ってくれるようになります。 さらに、モックオブジェクトを設定する際の matcher として isTrue() メソッドを使えるようにもなります。

PHPUnit_Framework_TestListener の実装

例 14.3 は、 PHPUnit_Framework_TestListener インターフェイスのシンプルな実装例です。

例 14.3: シンプルなテストリスナー

<?php
use PHPUnit\Framework\TestCase;

class SimpleTestListener implements PHPUnit_Framework_TestListener
{
    public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("テスト '%s' の実行中にエラーが発生\n", $test->getName());
    }

    public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
    {
        printf("テスト '%s' に失敗\n", $test->getName());
    }

    public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("テスト '%s' は未完成\n", $test->getName());
    }

    public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("テスト '%s' は危険\n", $test->getName());
    }

    public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
    {
        printf("テスト '%s' をスキップ\n", $test->getName());
    }

    public function startTest(PHPUnit_Framework_Test $test)
    {
        printf("テスト '%s' が開始\n", $test->getName());
    }

    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        printf("テスト '%s' が終了\n", $test->getName());
    }

    public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        printf("テストスイート '%s' が開始\n", $suite->getName());
    }

    public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
    {
        printf("テストスイート '%s' が終了\n", $suite->getName());
    }
}
?>

例 14.4 は、抽象クラス PHPUnit_Framework_BaseTestListener のサブクラスを作る例です。これは、インターフェイスのメソッドのうち実際に使うものだけを指定し、 他のメソッドについては空の実装を提供します。

例 14.4: ベーステストリスナーの利用法

<?php
use PHPUnit\Framework\TestCase;

class ShortTestListener extends PHPUnit_Framework_BaseTestListener
{
    public function endTest(PHPUnit_Framework_Test $test, $time)
    {
        printf("テスト '%s' が終了\n", $test->getName());
    }
}
?>

「テストリスナー」 に、自作のテストリスナーをテスト実行時にアタッチするための PHPUnit の設定方法についての説明があります。

PHPUnit_Extensions_TestDecorator のサブクラスの作成

PHPUnit_Extensions_TestDecorator のサブクラスでテストケースあるいはテストスイートをラッピングし、 デコレータパターンを使用することで 各テストの実行前後に何らかの処理をさせることができます。

PHPUnit には、PHPUnit_Extensions_RepeatedTest および PHPUnit_Extensions_TestSetup という 2 つの具象テストデコレータが付属しています。 前者はテストを繰り返し実行し、それらが全て成功した場合にのみ成功とみなします。 後者については 第 4 章 で説明しました。

例 14.5 は、テストデコレータ PHPUnit_Extensions_RepeatedTest の一部を抜粋したものです。独自のデコレータを作成するための参考にしてください。

例 14.5: RepeatedTest デコレータ

<?php
use PHPUnit\Framework\TestCase;

require_once 'PHPUnit/Extensions/TestDecorator.php';

class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator
{
    private $timesRepeat = 1;

    public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1)
    {
        parent::__construct($test);

        if (is_integer($timesRepeat) &&
            $timesRepeat >= 0) {
            $this->timesRepeat = $timesRepeat;
        }
    }

    public function count()
    {
        return $this->timesRepeat * $this->test->count();
    }

    public function run(PHPUnit_Framework_TestResult $result = null)
    {
        if ($result === null) {
            $result = $this->createResult();
        }

        for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) {
            $this->test->run($result);
        }

        return $result;
    }
}
?>

PHPUnit_Framework_Test の実装

PHPUnit_Framework_Test インターフェイスの機能は限られており、 実装するのは簡単です。PHPUnit_Framework_Test を実装するのは PHPUnit\Framework\TestCase の実装より単純で、 これを用いて例えば データ駆動のテスト (data-driven tests) などを実行します。

カンマ区切り (CSV) ファイルの値と比較する、データ駆動のテストを 例 14.6 に示します。このファイルの各行は foo;bar のような形式になっており (訳注: CSV じゃない……)、 最初の値が期待値で 2 番目の値が実際の値です。

例 14.6: データ駆動のテスト

<?php
use PHPUnit\Framework\TestCase;

class DataDrivenTest implements PHPUnit_Framework_Test
{
    private $lines;

    public function __construct($dataFile)
    {
        $this->lines = file($dataFile);
    }

    public function count()
    {
        return 1;
    }

    public function run(PHPUnit_Framework_TestResult $result = null)
    {
        if ($result === null) {
            $result = new PHPUnit_Framework_TestResult;
        }

        foreach ($this->lines as $line) {
            $result->startTest($this);
            PHP_Timer::start();
            $stopTime = null;

            list($expected, $actual) = explode(';', $line);

            try {
                PHPUnit_Framework_Assert::assertEquals(
                  trim($expected), trim($actual)
                );
            }

            catch (PHPUnit_Framework_AssertionFailedError $e) {
                $stopTime = PHP_Timer::stop();
                $result->addFailure($this, $e, $stopTime);
            }

            catch (Exception $e) {
                $stopTime = PHP_Timer::stop();
                $result->addError($this, $e, $stopTime);
            }

            if ($stopTime === null) {
                $stopTime = PHP_Timer::stop();
            }

            $result->endTest($this, $stopTime);
        }

        return $result;
    }
}

$test = new DataDrivenTest('data_file.csv');
$result = PHPUnit_TextUI_TestRunner::run($test);
?>
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds

There was 1 failure:

1) DataDrivenTest
Failed asserting that two strings are equal.
expected string <bar>
difference      <  x>
got string      <baz>
/home/sb/DataDrivenTest.php:32
/home/sb/DataDrivenTest.php:53

FAILURES!
Tests: 2, Failures: 1.

付録A アサーション

この節では、利用可能なアサーションメソッドの一覧を示します。

assertArrayHasKey()

assertArrayHasKey(mixed $key, array $array[, string $message = ''])

$array にキー $key が存在しない場合にエラー $message を報告します。

assertArrayNotHasKey() はこのアサーションの逆で、同じ引数をとります。

例 A.1: assertArrayHasKey() の使用法

<?php
use PHPUnit\Framework\TestCase;

class ArrayHasKeyTest extends TestCase
{
    public function testFailure()
    {
        $this->assertArrayHasKey('foo', ['bar' => 'baz']);
    }
}
?>
phpunit ArrayHasKeyTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ArrayHasKeyTest::testFailure
Failed asserting that an array has the key 'foo'.

/home/sb/ArrayHasKeyTest.php:6

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

assertClassHasAttribute()

assertClassHasAttribute(string $attributeName, string $className[, string $message = ''])

$className::attributeName が存在しない場合にエラー $message を報告します。

assertClassNotHasAttribute() はこのアサーションの逆で、同じ引数をとります。

例 A.2: assertClassHasAttribute() の使用法

<?php
use PHPUnit\Framework\TestCase;

class ClassHasAttributeTest extends TestCase
{
    public function testFailure()
    {
        $this->assertClassHasAttribute('foo', stdClass::class);
    }
}
?>
phpunit ClassHasAttributeTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ClassHasAttributeTest::testFailure
Failed asserting that class "stdClass" has attribute "foo".

/home/sb/ClassHasAttributeTest.php:6

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

assertArraySubset()

assertArraySubset(array $subset, array $array[, bool $strict = '', string $message = ''])

$array$subset を含まない場合にエラー $message を報告します。

$strict フラグを使うと、配列内のオブジェクトの比較にその識別子を利用します。

例 A.3: Usage of assertArraySubset()

<?php
use PHPUnit\Framework\TestCase;

class ArraySubsetTest extends TestCase
{
    public function testFailure()
    {
        $this->assertArraySubset(['config' => ['key-a', 'key-b']], ['config' => ['key-a']]);
    }
}
?>
phpunit ArrayHasKeyTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) Epilog\EpilogTest::testNoFollowOption
Failed asserting that an array has the subset Array &0 (
    'config' => Array &1 (
        0 => 'key-a'
        1 => 'key-b'
    )
).

/home/sb/ArraySubsetTest.php:6

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

assertClassHasStaticAttribute()

assertClassHasStaticAttribute(string $attributeName, string $className[, string $message = ''])

$className::attributeName が存在しない場合にエラー $message を報告します。

assertClassNotHasStaticAttribute() はこのアサーションの逆で、同じ引数をとります。

例 A.4: assertClassHasStaticAttribute() の使用法

<?php
use PHPUnit\Framework\TestCase;

class ClassHasStaticAttributeTest extends TestCase
{
    public function testFailure()
    {
        $this->assertClassHasStaticAttribute('foo', stdClass::class);
    }
}
?>
phpunit ClassHasStaticAttributeTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ClassHasStaticAttributeTest::testFailure
Failed asserting that class "stdClass" has static attribute "foo".

/home/sb/ClassHasStaticAttributeTest.php:6

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

assertContains()

assertContains(mixed $needle, Iterator|array $haystack[, string $message = ''])

$needle$haystack の要素でない場合にエラー $message を報告します。

assertNotContains() はこのアサーションの逆で、同じ引数をとります。

assertAttributeContains()assertAttributeNotContains() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性を haystack として使用することができます。

例 A.5: assertContains() の使用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContains(4, [1, 2, 3]);
    }
}
?>
phpunit ContainsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ContainsTest::testFailure
Failed asserting that an array contains 4.

/home/sb/ContainsTest.php:6

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

assertContains(string $needle, string $haystack[, string $message = '', boolean $ignoreCase = false])

$needle$haystack の部分文字列でない場合にエラー $message を報告します。

$ignoreCasetrue の場合、テストで大文字小文字を区別しなくなります。

例 A.6: assertContains() の使用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContains('baz', 'foobar');
    }
}
?>
phpunit ContainsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ContainsTest::testFailure
Failed asserting that 'foobar' contains "baz".

/home/sb/ContainsTest.php:6

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

例 A.7: assertContains() で $ignoreCase を使う方法

<?php
use PHPUnit\Framework\TestCase;

class ContainsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContains('foo', 'FooBar');
    }

    public function testOK()
    {
        $this->assertContains('foo', 'FooBar', '', true);
    }
}
?>
phpunit ContainsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F.

Time: 0 seconds, Memory: 2.75Mb

There was 1 failure:

1) ContainsTest::testFailure
Failed asserting that 'FooBar' contains "foo".

/home/sb/ContainsTest.php:6

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

assertContainsOnly()

assertContainsOnly(string $type, Iterator|array $haystack[, boolean $isNativeType = NULL, string $message = ''])

$haystack の中身の型が $type だけではない場合にエラー $message を報告します。

$isNativeType はフラグで、$type がネイティブな PHP の型であるかどうかを表します。

assertNotContainsOnly() はこのアサーションの逆で、同じ引数をとります。

assertAttributeContainsOnly()assertAttributeNotContainsOnly() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性を haystack として使用することができます。

例 A.8: assertContainsOnly() の使用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsOnlyTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContainsOnly('string', ['1', '2', 3]);
    }
}
?>
phpunit ContainsOnlyTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ContainsOnlyTest::testFailure
Failed asserting that Array (
    0 => '1'
    1 => '2'
    2 => 3
) contains only values of type "string".

/home/sb/ContainsOnlyTest.php:6

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

assertContainsOnlyInstancesOf()

assertContainsOnlyInstancesOf(string $classname, Traversable|array $haystack[, string $message = ''])

$haystack$classname クラスの唯一のインスタンスを含まない場合にエラー $message を報告します。

例 A.9: assertContainsOnlyInstancesOf() の使用法

<?php
use PHPUnit\Framework\TestCase;

class ContainsOnlyInstancesOfTest extends TestCase
{
    public function testFailure()
    {
        $this->assertContainsOnlyInstancesOf(
            Foo::class,
            [new Foo, new Bar, new Foo]
        );
    }
}
?>
phpunit ContainsOnlyInstancesOfTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) ContainsOnlyInstancesOfTest::testFailure
Failed asserting that Array ([0]=> Bar Object(...)) is an instance of class "Foo".

/home/sb/ContainsOnlyInstancesOfTest.php:6

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

assertCount()

assertCount($expectedCount, $haystack[, string $message = ''])

$haystack の要素数が $expectedCount でない場合にエラー $message を報告します。

assertNotCount() はこのアサーションの逆で、同じ引数をとります。

例 A.10: assertCount() の使用法

<?php
use PHPUnit\Framework\TestCase;

class CountTest extends TestCase
{
    public function testFailure()
    {
        $this->assertCount(0, ['foo']);
    }
}
?>
phpunit CountTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) CountTest::testFailure
Failed asserting that actual size 1 matches expected size 0.

/home/sb/CountTest.php:6

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

assertDirectoryExists()

assertDirectoryExists(string $directory[, string $message = ''])

$directory で指定したディレクトリが存在しない場合にエラー $message を報告します。

assertDirectoryNotExists() はこのアサーションの逆で、同じ引数をとります。

例 A.11: assertDirectoryExists() の使用法

<?php
use PHPUnit\Framework\TestCase;

class DirectoryExistsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertDirectoryExists('/path/to/directory');
    }
}
?>
phpunit DirectoryExistsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) DirectoryExistsTest::testFailure
Failed asserting that directory "/path/to/directory" exists.

/home/sb/DirectoryExistsTest.php:6

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

assertDirectoryIsReadable()

assertDirectoryIsReadable(string $directory[, string $message = ''])

$directory で指定したディレクトリが読み込み可能でない場合にエラー $message を報告します。

assertDirectoryNotIsReadable() はこのアサーションの逆で、同じ引数をとります。

例 A.12: assertDirectoryIsReadable() の使用法

<?php
use PHPUnit\Framework\TestCase;

class DirectoryIsReadableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertDirectoryIsReadable('/path/to/directory');
    }
}
?>
phpunit DirectoryIsReadableTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) DirectoryIsReadableTest::testFailure
Failed asserting that "/path/to/directory" is readable.

/home/sb/DirectoryIsReadableTest.php:6

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

assertDirectoryIsWritable()

assertDirectoryIsWritable(string $directory[, string $message = ''])

$directory で指定したディレクトリが書き込み可能でない場合にエラー $message を報告します。

assertDirectoryNotIsWritable() はこのアサーションの逆で、同じ引数をとります。

例 A.13: assertDirectoryIsWritable() の使用法

<?php
use PHPUnit\Framework\TestCase;

class DirectoryIsWritableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertDirectoryIsWritable('/path/to/directory');
    }
}
?>
phpunit DirectoryIsWritableTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) DirectoryIsWritableTest::testFailure
Failed asserting that "/path/to/directory" is writable.

/home/sb/DirectoryIsWritableTest.php:6

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

assertEmpty()

assertEmpty(mixed $actual[, string $message = ''])

$actual が空でない場合にエラー $message を報告します。

assertNotEmpty() はこのアサーションの逆で、同じ引数をとります。

assertAttributeEmpty() および assertAttributeNotEmpty() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性に対して使えます。

例 A.14: assertEmpty() の使用法

<?php
use PHPUnit\Framework\TestCase;

class EmptyTest extends TestCase
{
    public function testFailure()
    {
        $this->assertEmpty(['foo']);
    }
}
?>
phpunit EmptyTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) EmptyTest::testFailure
Failed asserting that an array is empty.

/home/sb/EmptyTest.php:6

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

assertEqualXMLStructure()

assertEqualXMLStructure(DOMElement $expectedElement, DOMElement $actualElement[, boolean $checkAttributes = false, string $message = ''])

$actualElement の DOMElement の XML 構造が $expectedElement の DOMElement の XML 構造と等しくない場合にエラー $message を報告します。

例 A.15: assertEqualXMLStructure() の使用法

<?php
use PHPUnit\Framework\TestCase;

class EqualXMLStructureTest extends TestCase
{
    public function testFailureWithDifferentNodeNames()
    {
        $expected = new DOMElement('foo');
        $actual = new DOMElement('bar');

        $this->assertEqualXMLStructure($expected, $actual);
    }

    public function testFailureWithDifferentNodeAttributes()
    {
        $expected = new DOMDocument;
        $expected->loadXML('<foo bar="true" />');

        $actual = new DOMDocument;
        $actual->loadXML('<foo/>');

        $this->assertEqualXMLStructure(
          $expected->firstChild, $actual->firstChild, true
        );
    }

    public function testFailureWithDifferentChildrenCount()
    {
        $expected = new DOMDocument;
        $expected->loadXML('<foo><bar/><bar/><bar/></foo>');

        $actual = new DOMDocument;
        $actual->loadXML('<foo><bar/></foo>');

        $this->assertEqualXMLStructure(
          $expected->firstChild, $actual->firstChild
        );
    }

    public function testFailureWithDifferentChildren()
    {
        $expected = new DOMDocument;
        $expected->loadXML('<foo><bar/><bar/><bar/></foo>');

        $actual = new DOMDocument;
        $actual->loadXML('<foo><baz/><baz/><baz/></foo>');

        $this->assertEqualXMLStructure(
          $expected->firstChild, $actual->firstChild
        );
    }
}
?>
phpunit EqualXMLStructureTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

FFFF

Time: 0 seconds, Memory: 5.75Mb

There were 4 failures:

1) EqualXMLStructureTest::testFailureWithDifferentNodeNames
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'foo'
+'bar'

/home/sb/EqualXMLStructureTest.php:9

2) EqualXMLStructureTest::testFailureWithDifferentNodeAttributes
Number of attributes on node "foo" does not match
Failed asserting that 0 matches expected 1.

/home/sb/EqualXMLStructureTest.php:22

3) EqualXMLStructureTest::testFailureWithDifferentChildrenCount
Number of child nodes of "foo" differs
Failed asserting that 1 matches expected 3.

/home/sb/EqualXMLStructureTest.php:35

4) EqualXMLStructureTest::testFailureWithDifferentChildren
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'

/home/sb/EqualXMLStructureTest.php:48

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

assertEquals()

assertEquals(mixed $expected, mixed $actual[, string $message = ''])

2 つの変数 $expected$actual が等しくない場合にエラー $message を報告します。

assertNotEquals() はこのアサーションの逆で、同じ引数をとります。

assertAttributeEquals()assertAttributeNotEquals() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性を実際の値として使用することができます。

例 A.16: assertEquals() の使用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertEquals(1, 0);
    }

    public function testFailure2()
    {
        $this->assertEquals('bar', 'baz');
    }

    public function testFailure3()
    {
        $this->assertEquals("foo\nbar\nbaz\n", "foo\nbah\nbaz\n");
    }
}
?>
phpunit EqualsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

FFF

Time: 0 seconds, Memory: 5.25Mb

There were 3 failures:

1) EqualsTest::testFailure
Failed asserting that 0 matches expected 1.

/home/sb/EqualsTest.php:6

2) EqualsTest::testFailure2
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'bar'
+'baz'

/home/sb/EqualsTest.php:11

3) EqualsTest::testFailure3
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
 'foo
-bar
+bah
 baz
 '

/home/sb/EqualsTest.php:16

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

引数 $expected$actual の型により特化した比較については、以下を参照ください。

assertEquals(float $expected, float $actual[, string $message = '', float $delta = 0])

2 つの float 値 $expected$actual の誤差が $delta より大きい場合にエラー $message を報告します。

なぜ $delta が必要となるのかについては "What Every Computer Scientist Should Know About Floating-Point Arithmetic" を参照ください。

例 A.17: float 値での assertEquals() の使用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testSuccess()
    {
        $this->assertEquals(1.0, 1.1, '', 0.2);
    }

    public function testFailure()
    {
        $this->assertEquals(1.0, 1.1);
    }
}
?>
phpunit EqualsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

.F

Time: 0 seconds, Memory: 5.75Mb

There was 1 failure:

1) EqualsTest::testFailure
Failed asserting that 1.1 matches expected 1.0.

/home/sb/EqualsTest.php:11

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

assertEquals(DOMDocument $expected, DOMDocument $actual[, string $message = ''])

2 つの DOMDocument オブジェクト $expected$actual で表される XML ドキュメントが (コメントを除去して正規化した状態で) 等しくない場合にエラー $message を報告します。

例 A.18: DOMDocument オブジェクトでの assertEquals() の使用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testFailure()
    {
        $expected = new DOMDocument;
        $expected->loadXML('<foo><bar/></foo>');

        $actual = new DOMDocument;
        $actual->loadXML('<bar><foo/></bar>');

        $this->assertEquals($expected, $actual);
    }
}
?>
phpunit EqualsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) EqualsTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
 <?xml version="1.0"?>
-<foo>
-  <bar/>
-</foo>
+<bar>
+  <foo/>
+</bar>

/home/sb/EqualsTest.php:12

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

assertEquals(object $expected, object $actual[, string $message = ''])

2 つのオブジェクト $expected$actual が同じ属性値を持たない場合にエラー $message を報告します。

例 A.19: オブジェクトでの assertEquals() の使用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testFailure()
    {
        $expected = new stdClass;
        $expected->foo = 'foo';
        $expected->bar = 'bar';

        $actual = new stdClass;
        $actual->foo = 'bar';
        $actual->baz = 'bar';

        $this->assertEquals($expected, $actual);
    }
}
?>
phpunit EqualsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) EqualsTest::testFailure
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
 stdClass Object (
-    'foo' => 'foo'
-    'bar' => 'bar'
+    'foo' => 'bar'
+    'baz' => 'bar'
 )

/home/sb/EqualsTest.php:14

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

assertEquals(array $expected, array $actual[, string $message = ''])

2 つの配列 $expected$actual が等しくない場合にエラー $message を報告します。

例 A.20: 配列での assertEquals() の使用法

<?php
use PHPUnit\Framework\TestCase;

class EqualsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertEquals(['a', 'b', 'c'], ['a', 'c', 'd']);
    }
}
?>
phpunit EqualsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) EqualsTest::testFailure
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     0 => 'a'
-    1 => 'b'
-    2 => 'c'
+    1 => 'c'
+    2 => 'd'
 )

/home/sb/EqualsTest.php:6

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

assertFalse()

assertFalse(bool $condition[, string $message = ''])

$conditiontrue の場合にエラー $message を報告します。

assertNotFalse() はこのアサーションの逆で、同じ引数をとります。

例 A.21: assertFalse() の使用法

<?php
use PHPUnit\Framework\TestCase;

class FalseTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFalse(true);
    }
}
?>
phpunit FalseTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) FalseTest::testFailure
Failed asserting that true is false.

/home/sb/FalseTest.php:6

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

assertFileEquals()

assertFileEquals(string $expected, string $actual[, string $message = ''])

$expected で指定したファイルと $actual で指定したファイルの内容が異なる場合にエラー $message を報告します。

assertFileNotEquals() はこのアサーションの逆で、同じ引数をとります。

例 A.22: assertFileEquals() の使用法

<?php
use PHPUnit\Framework\TestCase;

class FileEqualsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFileEquals('/home/sb/expected', '/home/sb/actual');
    }
}
?>
phpunit FileEqualsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) FileEqualsTest::testFailure
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'expected
+'actual
 '

/home/sb/FileEqualsTest.php:6

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

assertFileExists()

assertFileExists(string $filename[, string $message = ''])

ファイル $filename が存在しない場合にエラー $message を報告します。

assertFileNotExists() はこのアサーションの逆で、同じ引数をとります。

例 A.23: assertFileExists() の使用法

<?php
use PHPUnit\Framework\TestCase;

class FileExistsTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFileExists('/path/to/file');
    }
}
?>
phpunit FileExistsTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) FileExistsTest::testFailure
Failed asserting that file "/path/to/file" exists.

/home/sb/FileExistsTest.php:6

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

assertFileIsReadable()

assertFileIsReadable(string $filename[, string $message = ''])

$filename で指定したファイルが読み込み可能でない場合、あるいはファイルでない場合にエラー $message を報告します。

assertFileNotIsReadable() はこのアサーションの逆で、同じ引数をとります。

例 A.24: assertFileIsReadable() の使用法

<?php
use PHPUnit\Framework\TestCase;

class FileIsReadableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFileIsReadable('/path/to/file');
    }
}
?>
phpunit FileIsReadableTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) FileIsReadableTest::testFailure
Failed asserting that "/path/to/file" is readable.

/home/sb/FileIsReadableTest.php:6

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

assertFileIsWritable()

assertFileIsWritable(string $filename[, string $message = ''])

$filename で指定したファイルが書き込み可能でない場合、あるいはファイルでない場合にエラー $message を報告します。

assertFileNotIsWritable() はこのアサーションの逆で、同じ引数をとります。

例 A.25: assertFileIsWritable() の使用法

<?php
use PHPUnit\Framework\TestCase;

class FileIsWritableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertFileIsWritable('/path/to/file');
    }
}
?>
phpunit FileIsWritableTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) FileIsWritableTest::testFailure
Failed asserting that "/path/to/file" is writable.

/home/sb/FileIsWritableTest.php:6

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

assertGreaterThan()

assertGreaterThan(mixed $expected, mixed $actual[, string $message = ''])

$actual の値が $expected の値より大きくない場合にエラー $message を報告します。

assertAttributeGreaterThan() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性を実際の値として使用することができます。

例 A.26: assertGreaterThan() の使用法

<?php
use PHPUnit\Framework\TestCase;

class GreaterThanTest extends TestCase
{
    public function testFailure()
    {
        $this->assertGreaterThan(2, 1);
    }
}
?>
phpunit GreaterThanTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) GreaterThanTest::testFailure
Failed asserting that 1 is greater than 2.

/home/sb/GreaterThanTest.php:6

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

assertGreaterThanOrEqual()

assertGreaterThanOrEqual(mixed $expected, mixed $actual[, string $message = ''])

$actual の値が $expected の値以上でない場合にエラー $message を報告します。

assertAttributeGreaterThanOrEqual() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性を実際の値として使用することができます。

例 A.27: assertGreaterThanOrEqual() の使用法

<?php
use PHPUnit\Framework\TestCase;

class GreatThanOrEqualTest extends TestCase
{
    public function testFailure()
    {
        $this->assertGreaterThanOrEqual(2, 1);
    }
}
?>
phpunit GreaterThanOrEqualTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) GreatThanOrEqualTest::testFailure
Failed asserting that 1 is equal to 2 or is greater than 2.

/home/sb/GreaterThanOrEqualTest.php:6

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

assertInfinite()

assertInfinite(mixed $variable[, string $message = ''])

$variableINF でない場合にエラー $message を報告します。

assertFinite() はこのアサーションの逆で、同じ引数をとります。

例 A.28: assertInfinite() の使用法

<?php
use PHPUnit\Framework\TestCase;

class InfiniteTest extends TestCase
{
    public function testFailure()
    {
        $this->assertInfinite(1);
    }
}
?>
phpunit InfiniteTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) InfiniteTest::testFailure
Failed asserting that 1 is infinite.

/home/sb/InfiniteTest.php:6

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

assertInstanceOf()

assertInstanceOf($expected, $actual[, $message = ''])

$actual$expected のインスタンスでない場合にエラー $message を報告します。

assertNotInstanceOf() はこのアサーションの逆で、同じ引数をとります。

assertAttributeInstanceOf() および assertAttributeNotInstanceOf() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性に対して使えます。

例 A.29: assertInstanceOf() の使用法

<?php
use PHPUnit\Framework\TestCase;

class InstanceOfTest extends TestCase
{
    public function testFailure()
    {
        $this->assertInstanceOf(RuntimeException::class, new Exception);
    }
}
?>
phpunit InstanceOfTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) InstanceOfTest::testFailure
Failed asserting that Exception Object (...) is an instance of class "RuntimeException".

/home/sb/InstanceOfTest.php:6

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

assertInternalType()

assertInternalType($expected, $actual[, $message = ''])

$actual の型が $expected でない場合にエラー $message を報告します。

assertNotInternalType() はこのアサーションの逆で、同じ引数をとります。

assertAttributeInternalType() および assertAttributeNotInternalType() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性に対して使えます。

例 A.30: assertInternalType() の使用法

<?php
use PHPUnit\Framework\TestCase;

class InternalTypeTest extends TestCase
{
    public function testFailure()
    {
        $this->assertInternalType('string', 42);
    }
}
?>
phpunit InternalTypeTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) InternalTypeTest::testFailure
Failed asserting that 42 is of type "string".

/home/sb/InternalTypeTest.php:6

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

assertIsReadable()

assertIsReadable(string $filename[, string $message = ''])

$filename で指定したファイルあるいはディレクトリが読み込み可能でない場合にエラー $message を報告します。

assertNotIsReadable() はこのアサーションの逆で、同じ引数をとります。

例 A.31: assertIsReadable() の使用法

<?php
use PHPUnit\Framework\TestCase;

class IsReadableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertIsReadable('/path/to/unreadable');
    }
}
?>
phpunit IsReadableTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) IsReadableTest::testFailure
Failed asserting that "/path/to/unreadable" is readable.

/home/sb/IsReadableTest.php:6

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

assertIsWritable()

assertIsWritable(string $filename[, string $message = ''])

$filename で指定したファイルあるいはディレクトリが書き込み可能でない場合にエラー $message を報告します。

assertNotIsWritable() はこのアサーションの逆で、同じ引数をとります。

例 A.32: assertIsWritable() の使用法

<?php
use PHPUnit\Framework\TestCase;

class IsWritableTest extends TestCase
{
    public function testFailure()
    {
        $this->assertIsWritable('/path/to/unwritable');
    }
}
?>
phpunit IsWritableTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) IsWritableTest::testFailure
Failed asserting that "/path/to/unwritable" is writable.

/home/sb/IsWritableTest.php:6

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

assertJsonFileEqualsJsonFile()

assertJsonFileEqualsJsonFile(mixed $expectedFile, mixed $actualFile[, string $message = ''])

$actualFile の値が $expectedFile の値にマッチしない場合にエラー $message を報告します。

例 A.33: assertJsonFileEqualsJsonFile() の使用法

<?php
use PHPUnit\Framework\TestCase;

class JsonFileEqualsJsonFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertJsonFileEqualsJsonFile(
          'path/to/fixture/file', 'path/to/actual/file');
    }
}
?>
phpunit JsonFileEqualsJsonFileTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) JsonFileEqualsJsonFile::testFailure
Failed asserting that '{"Mascot":"Tux"}' matches JSON string "["Mascott", "Tux", "OS", "Linux"]".

/home/sb/JsonFileEqualsJsonFileTest.php:5

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

assertJsonStringEqualsJsonFile()

assertJsonStringEqualsJsonFile(mixed $expectedFile, mixed $actualJson[, string $message = ''])

$actualJson の値が $expectedFile の値にマッチしない場合にエラー $message を報告します。

例 A.34: assertJsonStringEqualsJsonFile() の使用法

<?php
use PHPUnit\Framework\TestCase;

class JsonStringEqualsJsonFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertJsonStringEqualsJsonFile(
            'path/to/fixture/file', json_encode(['Mascot' => 'ux'])
        );
    }
}
?>
phpunit JsonStringEqualsJsonFileTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) JsonStringEqualsJsonFile::testFailure
Failed asserting that '{"Mascot":"ux"}' matches JSON string "{"Mascott":"Tux"}".

/home/sb/JsonStringEqualsJsonFileTest.php:5

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

assertJsonStringEqualsJsonString()

assertJsonStringEqualsJsonString(mixed $expectedJson, mixed $actualJson[, string $message = ''])

$actualJson の値が $expectedJson の値にマッチしない場合にエラー $message を報告します。

例 A.35: assertJsonStringEqualsJsonString() の使用法

<?php
use PHPUnit\Framework\TestCase;

class JsonStringEqualsJsonStringTest extends TestCase
{
    public function testFailure()
    {
        $this->assertJsonStringEqualsJsonString(
            json_encode(['Mascot' => 'Tux']),
            json_encode(['Mascot' => 'ux'])
        );
    }
}
?>
phpunit JsonStringEqualsJsonStringTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) JsonStringEqualsJsonStringTest::testFailure
Failed asserting that two objects are equal.
--- Expected
+++ Actual
@@ @@
 stdClass Object (
 -    'Mascot' => 'Tux'
 +    'Mascot' => 'ux'
)

/home/sb/JsonStringEqualsJsonStringTest.php:5

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

assertLessThan()

assertLessThan(mixed $expected, mixed $actual[, string $message = ''])

$actual の値が $expected の値より小さくない場合にエラー $message を報告します。

assertAttributeLessThan() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性を実際の値として使用することができます。

例 A.36: assertLessThan() の使用法

<?php
use PHPUnit\Framework\TestCase;

class LessThanTest extends TestCase
{
    public function testFailure()
    {
        $this->assertLessThan(1, 2);
    }
}
?>
phpunit LessThanTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) LessThanTest::testFailure
Failed asserting that 2 is less than 1.

/home/sb/LessThanTest.php:6

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

assertLessThanOrEqual()

assertLessThanOrEqual(mixed $expected, mixed $actual[, string $message = ''])

$actual の値が $expected の値以下でない場合にエラー $message を報告します。

assertAttributeLessThanOrEqual() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性を実際の値として使用することができます。

例 A.37: assertLessThanOrEqual() の使用法

<?php
use PHPUnit\Framework\TestCase;

class LessThanOrEqualTest extends TestCase
{
    public function testFailure()
    {
        $this->assertLessThanOrEqual(1, 2);
    }
}
?>
phpunit LessThanOrEqualTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) LessThanOrEqualTest::testFailure
Failed asserting that 2 is equal to 1 or is less than 1.

/home/sb/LessThanOrEqualTest.php:6

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

assertNan()

assertNan(mixed $variable[, string $message = ''])

$variableNAN でない場合にエラー $message を報告します。

例 A.38: assertNan() の使用法

<?php
use PHPUnit\Framework\TestCase;

class NanTest extends TestCase
{
    public function testFailure()
    {
        $this->assertNan(1);
    }
}
?>
phpunit NanTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) NanTest::testFailure
Failed asserting that 1 is nan.

/home/sb/NanTest.php:6

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

assertNull()

assertNull(mixed $variable[, string $message = ''])

$variableNULL でないときにエラー $message を報告します。

assertNotNull() はこのアサーションの逆で、同じ引数をとります。

例 A.39: assertNull() の使用法

<?php
use PHPUnit\Framework\TestCase;

class NullTest extends TestCase
{
    public function testFailure()
    {
        $this->assertNull('foo');
    }
}
?>
phpunit NotNullTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) NullTest::testFailure
Failed asserting that 'foo' is null.

/home/sb/NotNullTest.php:6

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

assertObjectHasAttribute()

assertObjectHasAttribute(string $attributeName, object $object[, string $message = ''])

$object->attributeName が存在しない場合にエラー $message を報告します。

assertObjectNotHasAttribute() はこのアサーションの逆で、同じ引数をとります。

例 A.40: assertObjectHasAttribute() の使用法

<?php
use PHPUnit\Framework\TestCase;

class ObjectHasAttributeTest extends TestCase
{
    public function testFailure()
    {
        $this->assertObjectHasAttribute('foo', new stdClass);
    }
}
?>
phpunit ObjectHasAttributeTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) ObjectHasAttributeTest::testFailure
Failed asserting that object of class "stdClass" has attribute "foo".

/home/sb/ObjectHasAttributeTest.php:6

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

assertRegExp()

assertRegExp(string $pattern, string $string[, string $message = ''])

$string が正規表現 $pattern にマッチしない場合にエラー $message を報告します。

assertNotRegExp() はこのアサーションの逆で、同じ引数をとります。

例 A.41: assertRegExp() の使用法

<?php
use PHPUnit\Framework\TestCase;

class RegExpTest extends TestCase
{
    public function testFailure()
    {
        $this->assertRegExp('/foo/', 'bar');
    }
}
?>
phpunit RegExpTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) RegExpTest::testFailure
Failed asserting that 'bar' matches PCRE pattern "/foo/".

/home/sb/RegExpTest.php:6

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

assertStringMatchesFormat()

assertStringMatchesFormat(string $format, string $string[, string $message = ''])

$string が書式文字列 $format にマッチしない場合にエラー $message を報告します。

assertStringNotMatchesFormat() はこのアサーションの逆で、同じ引数をとります。

例 A.42: assertStringMatchesFormat() の使用法

<?php
use PHPUnit\Framework\TestCase;

class StringMatchesFormatTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringMatchesFormat('%i', 'foo');
    }
}
?>
phpunit StringMatchesFormatTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) StringMatchesFormatTest::testFailure
Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+$/s".

/home/sb/StringMatchesFormatTest.php:6

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

書式文字列には次のプレースホルダを含めることができます。

  • %e: ディレクトリ区切り文字、たとえば Linux なら / を表します。

  • %s: 一文字以上の何か (文字あるいは空白)、ただし改行文字は含みません。

  • %S: ゼロ文字以上の何か (文字あるいは空白)、ただし改行文字は含みません。

  • %a: 一文字以上の何か (文字あるいは空白)、改行文字も含みます。

  • %A: ゼロ文字以上の何か (文字あるいは空白)、改行文字も含みます。

  • %w: ゼロ文字以上の空白。

  • %i: 符号付き整数値。例: +3142, -3142

  • %d: 符号なし整数値。例: 123.66

  • %x: 一文字以上の十六進文字 (0-9, a-f, A-F)。

  • %f: 浮動小数点数値。例: 3.142, -3.142, 3.142E-10, 3.142e+10

  • %c: 任意の一文字。

assertStringMatchesFormatFile()

assertStringMatchesFormatFile(string $formatFile, string $string[, string $message = ''])

$string$formatFile の内容にマッチしない場合にエラー $message を報告します。

assertStringNotMatchesFormatFile() はこのアサーションの逆で、同じ引数をとります。

例 A.43: assertStringMatchesFormatFile() の使用法

<?php
use PHPUnit\Framework\TestCase;

class StringMatchesFormatFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringMatchesFormatFile('/path/to/expected.txt', 'foo');
    }
}
?>
phpunit StringMatchesFormatFileTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) StringMatchesFormatFileTest::testFailure
Failed asserting that 'foo' matches PCRE pattern "/^[+-]?\d+
$/s".

/home/sb/StringMatchesFormatFileTest.php:6

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

assertSame()

assertSame(mixed $expected, mixed $actual[, string $message = ''])

2 つの変数 $expected$actual が同じ型・同じ値でない場合にエラー $message を報告します。

assertNotSame() はこのアサーションの逆で、同じ引数をとります。

assertAttributeSame()assertAttributeNotSame() は便利なラッパーで、クラスやオブジェクトの publicprotectedprivate 属性を実際の値として使用することができます。

例 A.44: assertSame() の使用法

<?php
use PHPUnit\Framework\TestCase;

class SameTest extends TestCase
{
    public function testFailure()
    {
        $this->assertSame('2204', 2204);
    }
}
?>
phpunit SameTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) SameTest::testFailure
Failed asserting that 2204 is identical to '2204'.

/home/sb/SameTest.php:6

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

assertSame(object $expected, object $actual[, string $message = ''])

2 つの変数 $expected$actual が同じオブジェクトを参照していない場合にエラー $message を報告します。

例 A.45: オブジェクトでの assertSame() の使用法

<?php
use PHPUnit\Framework\TestCase;

class SameTest extends TestCase
{
    public function testFailure()
    {
        $this->assertSame(new stdClass, new stdClass);
    }
}
?>
phpunit SameTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 4.75Mb

There was 1 failure:

1) SameTest::testFailure
Failed asserting that two variables reference the same object.

/home/sb/SameTest.php:6

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

assertStringEndsWith()

assertStringEndsWith(string $suffix, string $string[, string $message = ''])

$string$suffix で終わっていない場合にエラー $message を報告します。

assertStringEndsNotWith() はこのアサーションの逆で、同じ引数をとります。

例 A.46: assertStringEndsWith() の使用法

<?php
use PHPUnit\Framework\TestCase;

class StringEndsWithTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringEndsWith('suffix', 'foo');
    }
}
?>
phpunit StringEndsWithTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 1 second, Memory: 5.00Mb

There was 1 failure:

1) StringEndsWithTest::testFailure
Failed asserting that 'foo' ends with "suffix".

/home/sb/StringEndsWithTest.php:6

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

assertStringEqualsFile()

assertStringEqualsFile(string $expectedFile, string $actualString[, string $message = ''])

$expectedFile で指定したファイルの内容に $actualString が含まれない場合にエラー $message を報告します。

assertStringNotEqualsFile() はこのアサーションの逆で、同じ引数をとります。

例 A.47: assertStringEqualsFile() の使用法

<?php
use PHPUnit\Framework\TestCase;

class StringEqualsFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringEqualsFile('/home/sb/expected', 'actual');
    }
}
?>
phpunit StringEqualsFileTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) StringEqualsFileTest::testFailure
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'expected
-'
+'actual'

/home/sb/StringEqualsFileTest.php:6

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

assertStringStartsWith()

assertStringStartsWith(string $prefix, string $string[, string $message = ''])

$string$prefix で始まっていない場合にエラー $message を報告します。

assertStringStartsNotWith() はこのアサーションの逆で、同じ引数をとります。

例 A.48: assertStringStartsWith() の使用法

<?php
use PHPUnit\Framework\TestCase;

class StringStartsWithTest extends TestCase
{
    public function testFailure()
    {
        $this->assertStringStartsWith('prefix', 'foo');
    }
}
?>
phpunit StringStartsWithTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) StringStartsWithTest::testFailure
Failed asserting that 'foo' starts with "prefix".

/home/sb/StringStartsWithTest.php:6

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

assertThat()

もっと複雑なアサーションを行う場合には、 PHPUnit_Framework_Constraint クラスを使用します。 これらは、assertThat() メソッドを使用して評価されます。 例 A.49 は、 logicalNot()equalTo() を用いて assertNotEquals() と同じアサーションを行う方法を示すものです。

assertThat(mixed $value, PHPUnit_Framework_Constraint $constraint[, $message = ''])

$value$constraint にマッチしない場合にエラー $message を報告します。

例 A.49: assertThat() の使用法

<?php
use PHPUnit\Framework\TestCase;

class BiscuitTest extends TestCase
{
    public function testEquals()
    {
        $theBiscuit = new Biscuit('Ginger');
        $myBiscuit  = new Biscuit('Ginger');

        $this->assertThat(
          $theBiscuit,
          $this->logicalNot(
            $this->equalTo($myBiscuit)
          )
        );
    }
}
?>

表 A.1 に、 使用できる PHPUnit_Framework_Constraint クラスをまとめます。

表A.1 制約

制約意味
PHPUnit_Framework_Constraint_Attribute attribute(PHPUnit_Framework_Constraint $constraint, $attributeName)別の制約を、クラスあるいはオブジェクトの属性として適用する制約。
PHPUnit_Framework_Constraint_IsAnything anything()あらゆる入力値を受け入れる制約。
PHPUnit_Framework_Constraint_ArrayHasKey arrayHasKey(mixed $key)配列が指定したキーを保持していることを保証する制約。
PHPUnit_Framework_Constraint_TraversableContains contains(mixed $value)Iterator インターフェイスを実装している array やオブジェクトが、指定した値を保持していることを保証する制約。
PHPUnit_Framework_Constraint_TraversableContainsOnly containsOnly(string $type)評価対象の array、あるいは Iterator インターフェイスを実装したオブジェクトが、指定した型の唯一の値を含むことを保証する制約。
PHPUnit_Framework_Constraint_TraversableContainsOnly containsOnlyInstancesOf(string $classname)評価対象の array、あるいは Iterator インターフェイスを実装したオブジェクトが、指定したクラスの唯一のインスタンスを含むことを保証する制約。
PHPUnit_Framework_Constraint_IsEqual equalTo($value, $delta = 0, $maxDepth = 10)ある値が別の値と等しいかどうかを調べる制約。
PHPUnit_Framework_Constraint_Attribute attributeEqualTo($attributeName, $value, $delta = 0, $maxDepth = 10)ある値がクラスあるいはオブジェクトの属性と等しいかどうかを調べる制約。
PHPUnit_Framework_Constraint_DirectoryExists directoryExists()指定した名前のディレクトリが存在するかどうかを調べる制約。
PHPUnit_Framework_Constraint_FileExists fileExists()指定した名前のファイルが存在するかどうかを調べる制約。
PHPUnit_Framework_Constraint_IsReadable isReadable()指定した名前のファイルが読み込み可能かどうかを調べる制約。
PHPUnit_Framework_Constraint_IsWritable isWritable()指定した名前のファイルが書き込み可能かどうかを調べる制約。
PHPUnit_Framework_Constraint_GreaterThan greaterThan(mixed $value)評価される値が、指定した値より大きいことを保証する制約。
PHPUnit_Framework_Constraint_Or greaterThanOrEqual(mixed $value)評価される値が、指定した値以上であることを保証する制約。
PHPUnit_Framework_Constraint_ClassHasAttribute classHasAttribute(string $attributeName)評価されるクラスに、指定した属性があることを保証する制約。
PHPUnit_Framework_Constraint_ClassHasStaticAttribute classHasStaticAttribute(string $attributeName)評価されるクラスに、指定した static 属性があることを保証する制約。
PHPUnit_Framework_Constraint_ObjectHasAttribute hasAttribute(string $attributeName)評価されるオブジェクトが、指定した属性を保持していることを保証する制約。
PHPUnit_Framework_Constraint_IsIdentical identicalTo(mixed $value)ある値が別の値と同一であることを保証する制約。
PHPUnit_Framework_Constraint_IsFalse isFalse()評価される値が false であることを保証する制約。
PHPUnit_Framework_Constraint_IsInstanceOf isInstanceOf(string $className)評価されるオブジェクトが、指定したクラスのインスタンスであることを保証する制約。
PHPUnit_Framework_Constraint_IsNull isNull()評価される値が NULL であることを保証する制約。
PHPUnit_Framework_Constraint_IsTrue isTrue()評価される値が true であることを保証する制約。
PHPUnit_Framework_Constraint_IsType isType(string $type)評価される値が、指定した型であることを保証する制約。
PHPUnit_Framework_Constraint_LessThan lessThan(mixed $value)評価される値が、指定した値より小さいことを保証する制約。
PHPUnit_Framework_Constraint_Or lessThanOrEqual(mixed $value)評価される値が、指定した値以下であることを保証する制約。
logicalAnd()論理積 (AND)。
logicalNot(PHPUnit_Framework_Constraint $constraint)論理否定 (NOT)。
logicalOr()論理和 (OR)。
logicalXor()排他的論理和 (XOR)。
PHPUnit_Framework_Constraint_PCREMatch matchesRegularExpression(string $pattern)評価される文字列が、正規表現にマッチすることを保証する制約。
PHPUnit_Framework_Constraint_StringContains stringContains(string $string, bool $case)評価される文字列が、指定した文字列を含むことを保証する制約。
PHPUnit_Framework_Constraint_StringEndsWith stringEndsWith(string $suffix)評価される文字列が、指定したサフィックスで終わることを保証する制約。
PHPUnit_Framework_Constraint_StringStartsWith stringStartsWith(string $prefix)評価される文字列が、指定したプレフィックスで始まることを保証する制約。

assertTrue()

assertTrue(bool $condition[, string $message = ''])

$conditionfalse の場合にエラー $message を報告します。

assertNotTrue() はこのアサーションの逆で、同じ引数をとります。

例 A.50: assertTrue() の使用法

<?php
use PHPUnit\Framework\TestCase;

class TrueTest extends TestCase
{
    public function testFailure()
    {
        $this->assertTrue(false);
    }
}
?>
phpunit TrueTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) TrueTest::testFailure
Failed asserting that false is true.

/home/sb/TrueTest.php:6

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

assertXmlFileEqualsXmlFile()

assertXmlFileEqualsXmlFile(string $expectedFile, string $actualFile[, string $message = ''])

$actualFile の XML ドキュメントが $expectedFile の XML ドキュメントと異なる場合にエラー $message を報告します。

assertXmlFileNotEqualsXmlFile() はこのアサーションの逆で、同じ引数をとります。

例 A.51: assertXmlFileEqualsXmlFile() の使用法

<?php
use PHPUnit\Framework\TestCase;

class XmlFileEqualsXmlFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertXmlFileEqualsXmlFile(
          '/home/sb/expected.xml', '/home/sb/actual.xml');
    }
}
?>
phpunit XmlFileEqualsXmlFileTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) XmlFileEqualsXmlFileTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
 <?xml version="1.0"?>
 <foo>
-  <bar/>
+  <baz/>
 </foo>

/home/sb/XmlFileEqualsXmlFileTest.php:7

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

assertXmlStringEqualsXmlFile()

assertXmlStringEqualsXmlFile(string $expectedFile, string $actualXml[, string $message = ''])

$actualXml の XML ドキュメントが $expectedFile の XML ドキュメントと異なる場合にエラー $message を報告します。

assertXmlStringNotEqualsXmlFile() はこのアサーションの逆で、同じ引数をとります。

例 A.52: assertXmlStringEqualsXmlFile() の使用法

<?php
use PHPUnit\Framework\TestCase;

class XmlStringEqualsXmlFileTest extends TestCase
{
    public function testFailure()
    {
        $this->assertXmlStringEqualsXmlFile(
          '/home/sb/expected.xml', '<foo><baz/></foo>');
    }
}
?>
phpunit XmlStringEqualsXmlFileTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.25Mb

There was 1 failure:

1) XmlStringEqualsXmlFileTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
 <?xml version="1.0"?>
 <foo>
-  <bar/>
+  <baz/>
 </foo>

/home/sb/XmlStringEqualsXmlFileTest.php:7

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

assertXmlStringEqualsXmlString()

assertXmlStringEqualsXmlString(string $expectedXml, string $actualXml[, string $message = ''])

$actualXml の XML ドキュメントが $expectedXml の XML ドキュメントと異なる場合にエラー $message を報告します。

assertXmlStringNotEqualsXmlString() はこのアサーションの逆で、同じ引数をとります。

例 A.53: assertXmlStringEqualsXmlString() の使用法

<?php
use PHPUnit\Framework\TestCase;

class XmlStringEqualsXmlStringTest extends TestCase
{
    public function testFailure()
    {
        $this->assertXmlStringEqualsXmlString(
          '<foo><bar/></foo>', '<foo><baz/></foo>');
    }
}
?>
phpunit XmlStringEqualsXmlStringTest
PHPUnit 6.1.0 by Sebastian Bergmann and contributors.

F

Time: 0 seconds, Memory: 5.00Mb

There was 1 failure:

1) XmlStringEqualsXmlStringTest::testFailure
Failed asserting that two DOM documents are equal.
--- Expected
+++ Actual
@@ @@
 <?xml version="1.0"?>
 <foo>
-  <bar/>
+  <baz/>
 </foo>

/home/sb/XmlStringEqualsXmlStringTest.php:7

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

付録B アノテーション

アノテーションとはメタデータを表す特別な構文のことで、 プログラミング言語のソースコードに追加することができます。 PHP そのものにはソースコードにアノテーションする専用の仕組みはありませんが、 ドキュメンテーションブロックに @アノテーション名 引数 のようなタグを書くことでアノテーションを表すという記法が PHP コミュニティ内で一般に使われています。 PHP では、リフレクション API の getDocComment() メソッドを使えば関数、クラス、メソッド、属性 それぞれのドキュメンテーションブロックにアクセスすることができます。 PHPUnit などのアプリケーションでは、 この情報をもとに実行時の振る舞いを設定するのです。

注記

PHP の doc コメントは、/** で始めて */ で終わる必要があります。 その他の形式のコメントで書いたアノテーションは、無視されます。

本章では、PHPUnit がサポートするすべてのアノテーションについて解説します。

@author

@author アノテーションは @group アノテーション (「@group」 を参照ください) のエイリアスで、 テストの作者にもとづいたフィルタリングができるようになります。

@after

@after アノテーションを使うと、 テストケースクラス内の各テストメソッドを実行した後に呼ぶメソッドを指定できます。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @after
     */
    public function tearDownSomeFixtures()
    {
        // ...
    }

    /**
     * @after
     */
    public function tearDownSomeOtherFixtures()
    {
        // ...
    }
}

@afterClass

@afterClass アノテーションを使うと、 テストケースクラス内の各テストメソッドを実行した後に呼ぶ静的メソッドを指定できます。 ここで共有フィクスチャの後始末をします。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @afterClass
     */
    public static function tearDownSomeSharedFixtures()
    {
        // ...
    }

    /**
     * @afterClass
     */
    public static function tearDownSomeOtherSharedFixtures()
    {
        // ...
    }
}

@backupGlobals

グローバル変数の保存や復元を、テストケースクラスのすべてのテストで完全に無効にすることができます。 このように使います。

use PHPUnit\Framework\TestCase;

/**
 * @backupGlobals disabled
 */
class MyTest extends TestCase
{
    // ...
}

@backupGlobals アノテーションは、テストメソッドレベルで使うこともできます。 これによって、保存と復元の操作をより細やかに制御できるようになります。

use PHPUnit\Framework\TestCase;

/**
 * @backupGlobals disabled
 */
class MyTest extends TestCase
{
    /**
     * @backupGlobals enabled
     */
    public function testThatInteractsWithGlobalVariables()
    {
        // ...
    }
}

@backupStaticAttributes

@backupStaticAttributes アノテーションを使うと、 宣言されたクラス内のすべての static プロパティの値をバックアップしてからテストを始め、 テストが終わった後でそれらの値を復元することができます。 テストケースクラス単位、あるいはテストメソッド単位で使えます。

use PHPUnit\Framework\TestCase;

/**
 * @backupStaticAttributes enabled
 */
class MyTest extends TestCase
{
    /**
     * @backupStaticAttributes disabled
     */
    public function testThatInteractsWithStaticAttributes()
    {
        // ...
    }
}

注記

PHP の内部的な制約のため、 @backupStaticAttributes が、 意図していない static 値を保存してしまい、 その後のテストに影響してしまうことがあります。

詳細は 「グローバルな状態」 を参照ください。

@before

@before アノテーションを使うと、 テストケースクラス内の各テストメソッドを実行する前に呼ぶメソッドを指定できます。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @before
     */
    public function setupSomeFixtures()
    {
        // ...
    }

    /**
     * @before
     */
    public function setupSomeOtherFixtures()
    {
        // ...
    }
}

@beforeClass

@beforeClass アノテーションを使うと、 テストケースクラス内の各テストメソッドを実行する前に呼ぶ静的メソッドを指定できます。 ここで共有フィクスチャの準備をします。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @beforeClass
     */
    public static function setUpSomeSharedFixtures()
    {
        // ...
    }

    /**
     * @beforeClass
     */
    public static function setUpSomeOtherSharedFixtures()
    {
        // ...
    }
}

@codeCoverageIgnore*

@codeCoverageIgnore@codeCoverageIgnoreStart、そして @codeCoverageIgnoreEnd アノテーションを使うと、 コード内の特定の行をカバレッジ解析の対象外にできます。

利用法は 「コードブロックの無視」 を参照ください。

@covers

@covers アノテーションをテストコードで使うと、 そのテストメソッドがどのメソッドをテストするのかを指定することができます。

/**
 * @covers BankAccount::getBalance
 */
public function testBalanceIsInitiallyZero()
{
    $this->assertEquals(0, $this->ba->getBalance());
}

これを指定した場合は、指定したメソッドのみのコードカバレッジ情報を考慮することになります。

表 B.1@covers アノテーションの構文を示します。

表B.1 カバーするメソッドを指定するためのアノテーション

アノテーション説明
@covers ClassName::methodNameそのテストメソッドが指定したメソッドをカバーすることを表します。
@covers ClassNameそのテストメソッドが指定したクラスのすべてのメソッドをカバーすることを表します。
@covers ClassName<extended>そのテストメソッドが、指定したクラスとその親クラスおよびインターフェイスのすべてのメソッドをカバーすることを表します。
@covers ClassName::<public>そのテストメソッドが、指定したクラスのすべての public メソッドをカバーすることを表します。
@covers ClassName::<protected>そのテストメソッドが、指定したクラスのすべての protected メソッドをカバーすることを表します。
@covers ClassName::<private>そのテストメソッドが、指定したクラスのすべての private メソッドをカバーすることを表します。
@covers ClassName::<!public>そのテストメソッドが、指定したクラスのすべての非 public メソッドをカバーすることを表します。
@covers ClassName::<!protected>そのテストメソッドが、指定したクラスのすべての非 protected メソッドをカバーすることを表します。
@covers ClassName::<!private>そのテストメソッドが、指定したクラスのすべての非 private メソッドをカバーすることを表します。
@covers ::functionNameそのテストメソッドが、指定したグローバル関数をカバーすることを表します。

@coversDefaultClass

@coversDefaultClass アノテーションを使うと、 デフォルトの名前空間あるいはクラス名を指定できます。 こうすることで、 @covers アノテーションのたびに長い名前を繰り返す必要がなくなります。 例 B.1 を参照ください。

例 B.1: @coversDefaultClass を使ったアノテーションの短縮

<?php
use PHPUnit\Framework\TestCase;

/**
 * @coversDefaultClass \Foo\CoveredClass
 */
class CoversDefaultClassTest extends TestCase
{
    /**
     * @covers ::publicMethod
     */
    public function testSomething()
    {
        $o = new Foo\CoveredClass;
        $o->publicMethod();
    }
}
?>

@coversNothing

@coversNothing アノテーションをテストコードで使うと、 そのテストケースについてはコードカバレッジ情報を記録しないように指定できます。

これはインテグレーションテストで使えます。例として 例 11.3 を参照ください。

このメソッドはクラスレベルおよびメソッドレベルで使え、 あらゆる @covers タグを上書きします。

@dataProvider

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

詳細は 「データプロバイダ」 を参照ください。

@depends

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

詳細は 「テストの依存性」 を参照ください。

@expectedException

例 2.10 は、テストするコード内で例外がスローされたかどうかを @expectedException アノテーションを使用して調べる方法を示すものです。

詳細は 「例外のテスト」 を参照ください。

@expectedExceptionCode

@expectedExceptionCode アノテーションを @expectedException と組み合わせて使うと、 スローされた例外のエラーコードについてのアサーションが可能となり、 例外をより狭い範囲に特定できるようになります。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @expectedException     MyException
     * @expectedExceptionCode 20
     */
    public function testExceptionHasErrorcode20()
    {
        throw new MyException('Some Message', 20);
    }
}

テストを実行しやすくし、重複を減らすために、 ショートカットを使ってクラス定数を指定することができます。 @expectedExceptionCode で "@expectedExceptionCode ClassName::CONST" のようにして使います。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
      * @expectedException     MyException
      * @expectedExceptionCode MyClass::ERRORCODE
      */
    public function testExceptionHasErrorcode20()
    {
      throw new MyException('Some Message', 20);
    }
}
class MyClass
{
    const ERRORCODE = 20;
}

@expectedExceptionMessage

@expectedExceptionMessage アノテーションは @expectedExceptionCode と似ており、 例外のエラーメッセージに関するアサーションを行います。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @expectedException        MyException
     * @expectedExceptionMessage Some Message
     */
    public function testExceptionHasRightMessage()
    {
        throw new MyException('Some Message', 20);
    }
}

期待するメッセージを、例外メッセージの一部にすることもできます。 これは、特定の名前や渡したパラメータが例外に表示されることを確かめたいけれども 例外メッセージ全体は固定していない場合に便利です。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
     /**
      * @expectedException        MyException
      * @expectedExceptionMessage broken
      */
     public function testExceptionHasRightMessage()
     {
         $param = "broken";
         throw new MyException('Invalid parameter "'.$param.'".', 20);
     }
}

テストを実行しやすくし、重複を減らすために、 ショートカットを使ってクラス定数を指定することができます。 @expectedExceptionMessage で "@expectedExceptionMessage ClassName::CONST" のようにして使います。 サンプルコードは 「@expectedExceptionCode」 を参照ください。

@expectedExceptionMessageRegExp

期待するメッセージを、@expectedExceptionMessageRegExp アノテーションを使って正規表現で指定することもできます。 これは、部分文字列だけでは指定したメッセージとのマッチングが不十分なときに便利です。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
     /**
      * @expectedException              MyException
      * @expectedExceptionMessageRegExp /Argument \d+ can not be an? \w+/
      */
     public function testExceptionHasRightMessage()
     {
         throw new MyException('Argument 2 can not be an integer');
     }
}

@group

あるテストを、ひとつあるいは複数のグループに属するものとすることができます。 @group アノテーションをこのように使用します。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @group specification
     */
    public function testSomething()
    {
    }

    /**
     * @group regresssion
     * @group bug2204
     */
    public function testSomethingElse()
    {
    }
}

特定のグループに属するテストのみを選んで実行するには、 コマンドラインのテストランナーの場合は --group オプションあるいは --exclude-group オプションを指定します。XML 設定ファイルの場合は、 それぞれ対応するディレクティブを指定します。

@large

@large アノテーションは、 @group large のエイリアスです。

PHP_Invoker パッケージがインストールされていて strict モードが有効な場合に、 large テストは実行時間が 60 秒を超えたら失敗します。 このタイムアウト時間は、XML 設定ファイルの timeoutForLargeTests 属性で変更できます。

@medium

@medium アノテーションは @group medium のエイリアスです。 medium テストは、@large とマークしたテストに依存してはいけません。

PHP_Invoker パッケージがインストールされていて strict モードが有効な場合に、 medium テストは実行時間が 10 秒を超えたら失敗します。 このタイムアウト時間は、XML 設定ファイルの timeoutForMediumTests 属性で変更できます。

@preserveGlobalState

テストを別プロセスで実行するときに、 PHPUnit は親プロセスのグローバルな状態を保存しようと試みます。 親プロセスのすべてのグローバル状態をシリアライズし、 子プロセス内で最後にそれをアンシリアライズするのです。 しかし、親プロセスのグローバル状態の中にもしシリアライズできないものがあれば、 問題が発生します。この問題に対応するために、グローバル状態の保存を無効にすることができます。 そのために使うのが @preserveGlobalState アノテーションです。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     */
    public function testInSeparateProcess()
    {
        // ...
    }
}

@requires

@requires アノテーションを使うと、共通の事前条件 (たとえば PHP のバージョンや拡張モジュールのインストール状況) を満たさないときにテストをスキップできます。

条件に指定できる内容やその例については 表 7.3 を参照ください。

@runTestsInSeparateProcesses

テストクラス内のすべてのテストケースを、個別の PHP プロセスで実行するように指示します。

use PHPUnit\Framework\TestCase;

/**
 * @runTestsInSeparateProcesses
 */
class MyTest extends TestCase
{
    // ...
}

注意: デフォルトで、PHPUnit は親プロセスのグローバルな状態を保存しようと試みます。 親プロセスのすべてのグローバル状態をシリアライズし、 子プロセス内で最後にそれをアンシリアライズするのです。 しかし、親プロセスのグローバル状態の中にもしシリアライズできないものがあれば、 問題が発生します。この問題に対応するために、グローバル状態の保存を無効にすることができます。 この問題の対処法については 「@preserveGlobalState」 を参照ください。

@runInSeparateProcess

そのテストを個別の PHP プロセスで実行するように指示します。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @runInSeparateProcess
     */
    public function testInSeparateProcess()
    {
        // ...
    }
}

注意: デフォルトで、PHPUnit は親プロセスのグローバルな状態を保存しようと試みます。 親プロセスのすべてのグローバル状態をシリアライズし、 子プロセス内で最後にそれをアンシリアライズするのです。 しかし、親プロセスのグローバル状態の中にもしシリアライズできないものがあれば、 問題が発生します。この問題に対応するために、グローバル状態の保存を無効にすることができます。 この問題の対処法については 「@preserveGlobalState」 を参照ください。

@small

@small アノテーションは @group small のエイリアスです。 small テストは、@medium@large とマークしたテストに依存してはいけません。

PHP_Invoker パッケージがインストールされていて strict モードが有効な場合に、 small テストは実行時間が 1 秒を超えたら失敗します。 このタイムアウト時間は、XML 設定ファイルの timeoutForSmallTests 属性で変更できます。

注記

テストの実行時間の制限を有効にするには、@small@medium@large のいずれかのアノテーションで明示的に指定する必要があります。

@test

テストメソッド名の先頭に test をつけるかわりに、メソッドのドキュメンテーションブロックで @test アノテーションを使ってそのメソッドがテストメソッドであることを指定することができます。

/**
 * @test
 */
public function initialBalanceShouldBe0()
{
    $this->assertEquals(0, $this->ba->getBalance());
}

@testdox

@ticket

@uses

@uses アノテーションは、 テストから実行されてはいるが、そのテストでカバーするつもりはないコードを指定します。 たとえば、コード片をテストするために必要な値オブジェクトなどに使います。

/**
 * @covers BankAccount::deposit
 * @uses   Money
 */
public function testMoneyCanBeDepositedInAccount()
{
    // ...
}

このアノテーションは、厳密なカバレッジモードで使うと特に有用です。 このモードの場合、意図せずカバーしてしまったコードがテストを失敗させてしまうことがあるからです。 厳密なカバレッジモードに関する詳細な情報は 「意図せぬうちにカバーされているコード」 を参照ください。

付録C XML 設定ファイル

PHPUnit

<phpunit> 要素の属性を使って PHPUnit のコア機能を設定します。

<phpunit
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"
         backupGlobals="true"
         backupStaticAttributes="false"
         <!--bootstrap="/path/to/bootstrap.php"-->
         cacheTokens="false"
         colors="false"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         forceCoversAnnotation="false"
         mapTestClassNameToCoveredClassName="false"
         printerClass="PHPUnit_TextUI_ResultPrinter"
         <!--printerFile="/path/to/ResultPrinter.php"-->
         processIsolation="false"
         stopOnError="false"
         stopOnFailure="false"
         stopOnIncomplete="false"
         stopOnSkipped="false"
         stopOnRisky="false"
         testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"
         <!--testSuiteLoaderFile="/path/to/StandardTestSuiteLoader.php"-->
         timeoutForSmallTests="1"
         timeoutForMediumTests="10"
         timeoutForLargeTests="60"
         verbose="false">
  <!-- ... -->
</phpunit>

上の XML 設定ファイルは、TextUI テストランナーをデフォルトの設定で起動します。 その詳細は 「コマンドラインオプション」 で説明します。

その他、コマンドラインからは設定できないオプションもあります。

convertErrorsToExceptions

false にすると、 すべての PHP のエラーを例外に変換するエラーハンドラをインストールしません。

convertNoticesToExceptions

false にすると、 convertErrorsToExceptions でインストールしたエラーハンドラが E_NOTICEE_USER_NOTICE そして E_STRICT を例外に変換しなくなります。

convertWarningsToExceptions

false にすると、 convertErrorsToExceptions でインストールしたエラーハンドラが E_WARNINGE_USER_WARNING を例外に変換しなくなります。

forceCoversAnnotation

コードカバレッジの記録を、 @covers アノテーションを使っている関数だけに限定します。 このアノテーションについては 「@covers」 で説明します。

timeoutForLargeTests

テストのサイズによるタイムアウト値を制限する場合に、 @large とマークされたすべてのテストのタイムアウトをこの属性で設定します。 ここで指定した時間内にテストが完了しなかった場合、テストは失敗します。

timeoutForMediumTests

テストのサイズによるタイムアウト値を制限する場合に、 @medium とマークされたすべてのテストのタイムアウトをこの属性で設定します。 ここで指定した時間内にテストが完了しなかった場合、テストは失敗します。

timeoutForSmallTests

テストのサイズによるタイムアウト値を制限する場合に、 @medium あるいは @large のいずれのマークもついていないすべてのテストのタイムアウトをこの属性で設定します。 ここで指定した時間内にテストが完了しなかった場合、テストは失敗します。

テストスイート

<testsuites> 要素とその子要素である <testsuite> を使って、 テストスイート群やテストケース群の中からテストスイートを構成します。

<testsuites>
  <testsuite name="My Test Suite">
    <directory>/path/to/*Test.php files</directory>
    <file>/path/to/MyTest.php</file>
    <exclude>/path/to/exclude</exclude>
  </testsuite>
</testsuites>

phpVersion および phpVersionOperator 属性を使うと、必要な PHP のバージョンを指定できます。次の例は、PHP のバージョンが 5.3.0 以降である場合にのみ /path/to/*Test.php/path/to/MyTest.php を追加します。

  <testsuites>
    <testsuite name="My Test Suite">
      <directory suffix="Test.php" phpVersion="5.3.0" phpVersionOperator=">=">/path/to/files</directory>
      <file phpVersion="5.3.0" phpVersionOperator=">=">/path/to/MyTest.php</file>
    </testsuite>
  </testsuites>

phpVersionOperator 属性はオプションで、デフォルトは >= です。

グループ

<groups> 要素とその子要素である <include><exclude> および <group> を使って、 @group アノテーション (「@group」 を参照ください) でマークされたテストグループから実行する (しない) ものを選びます。

<groups>
  <include>
    <group>name</group>
  </include>
  <exclude>
    <group>name</group>
  </exclude>
</groups>

上の XML 設定ファイルは、 TextUI テストランナーを以下の引数で起動します。

  • --group name

  • --exclude-group name

コードカバレッジ対象のファイルのホワイトリスト

<filter> 要素とその子要素を使って、 コードカバレッジレポートのホワイトリストを設定します。

<filter>
  <whitelist processUncoveredFilesFromWhitelist="true">
    <directory suffix=".php">/path/to/files</directory>
    <file>/path/to/file</file>
    <exclude>
      <directory suffix=".php">/path/to/files</directory>
      <file>/path/to/file</file>
    </exclude>
  </whitelist>
</filter>

ログ出力

<logging> 要素とその子要素である <log> を使って、 テストの実行結果のログ出力を設定します。

<logging>
  <log type="coverage-html" target="/tmp/report" lowUpperBound="35"
       highLowerBound="70"/>
  <log type="coverage-clover" target="/tmp/coverage.xml"/>
  <log type="coverage-php" target="/tmp/coverage.serialized"/>
  <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
  <log type="json" target="/tmp/logfile.json"/>
  <log type="tap" target="/tmp/logfile.tap"/>
  <log type="junit" target="/tmp/logfile.xml" logIncompleteSkipped="false"/>
  <log type="testdox-html" target="/tmp/testdox.html"/>
  <log type="testdox-text" target="/tmp/testdox.txt"/>
</logging>

上の XML 設定ファイルは、 TextUI テストランナーを以下の引数で起動します。

  • --coverage-html /tmp/report

  • --coverage-clover /tmp/coverage.xml

  • --coverage-php /tmp/coverage.serialized

  • --coverage-text

  • --log-json /tmp/logfile.json

  • > /tmp/logfile.txt

  • --log-tap /tmp/logfile.tap

  • --log-junit /tmp/logfile.xml

  • --testdox-html /tmp/testdox.html

  • --testdox-text /tmp/testdox.txt

lowUpperBoundhighLowerBoundlogIncompleteSkipped および showUncoveredFiles 属性には、TextUI テストランナーで対応するオプションがありません。

  • lowUpperBound: カバー率がこの値に満たないときに、カバー率が "低い" とみなします。

  • highLowerBound: カバー率がこの値を超えるときに、カバー率が "高い" とみなします。

  • showUncoveredFiles: --coverage-text の出力で、カバレッジ情報だけではなくホワイトリストの全ファイル一覧も表示します。

  • showOnlySummary: Show only the summary in --coverage-text output.

テストリスナー

<listeners> 要素とその子要素である <listener> を使って、 テスト実行時にテストリスナーをアタッチします。

<listeners>
  <listener class="MyListener" file="/optional/path/to/MyListener.php">
    <arguments>
      <array>
        <element key="0">
          <string>Sebastian</string>
        </element>
      </array>
      <integer>22</integer>
      <string>April</string>
      <double>19.78</double>
      <null/>
      <object class="stdClass"/>
    </arguments>
  </listener>
</listeners>

上の XML 設定は、 $listener オブジェクト (以下を参照ください) をテストの実行時にアタッチします。

$listener = new MyListener(
    ['Sebastian'],
    22,
    'April',
    19.78,
    null,
    new stdClass
);

PHP INI 項目や定数、グローバル変数の設定

<php> 要素とその子要素を使って、 PHP の設定や定数、グローバル変数を設定します。また、 include_path の先頭にパスを追加することもできます。

<php>
  <includePath>.</includePath>
  <ini name="foo" value="bar"/>
  <const name="foo" value="bar"/>
  <var name="foo" value="bar"/>
  <env name="foo" value="bar"/>
  <post name="foo" value="bar"/>
  <get name="foo" value="bar"/>
  <cookie name="foo" value="bar"/>
  <server name="foo" value="bar"/>
  <files name="foo" value="bar"/>
  <request name="foo" value="bar"/>
</php>

上の XML 設定は、次の PHP コードに対応します。

ini_set('foo', 'bar');
define('foo', 'bar');
$GLOBALS['foo'] = 'bar';
$_ENV['foo'] = 'bar';
$_POST['foo'] = 'bar';
$_GET['foo'] = 'bar';
$_COOKIE['foo'] = 'bar';
$_SERVER['foo'] = 'bar';
$_FILES['foo'] = 'bar';
$_REQUEST['foo'] = 'bar';

付録D 目次

A

Agile Documentation, コマンドラインオプション
Agile Documentation (アジャイルな文書作成), アジャイルな文書作成
Annotation, PHPUnit 用のテストの書き方, テストの依存性, データプロバイダ, 例外のテスト, コマンドラインオプション, コードブロックの無視, カバーするメソッドの指定, アノテーション
anything(), assertThat()
arrayHasKey(), assertThat()
assertArrayHasKey(), assertArrayHasKey()
assertArrayNotHasKey(), assertArrayHasKey()
assertArraySubset(), assertArraySubset()
assertAttributeContains(), assertContains()
assertAttributeContainsOnly(), assertContainsOnly()
assertAttributeEmpty(), assertEmpty()
assertAttributeEquals(), assertEquals()
assertAttributeGreaterThan(), assertGreaterThan()
assertAttributeGreaterThanOrEqual(), assertGreaterThanOrEqual()
assertAttributeInstanceOf(), assertInstanceOf()
assertAttributeInternalType(), assertInternalType()
assertAttributeLessThan(), assertLessThan()
assertAttributeLessThanOrEqual(), assertLessThanOrEqual()
assertAttributeNotContains(), assertContains()
assertAttributeNotContainsOnly(), assertContainsOnly()
assertAttributeNotEmpty(), assertEmpty()
assertAttributeNotEquals(), assertEquals()
assertAttributeNotInstanceOf(), assertInstanceOf()
assertAttributeNotInternalType(), assertInternalType()
assertAttributeNotSame(), assertSame()
assertAttributeSame(), assertSame()
assertClassHasAttribute(), assertClassHasAttribute()
assertClassHasStaticAttribute(), assertClassHasStaticAttribute()
assertClassNotHasAttribute(), assertClassHasAttribute()
assertClassNotHasStaticAttribute(), assertClassHasStaticAttribute()
assertContains(), assertContains()
assertContainsOnly(), assertContainsOnly()
assertContainsOnlyInstancesOf(), assertContainsOnlyInstancesOf()
assertCount(), assertCount()
assertDirectoryExists(), assertDirectoryExists()
assertDirectoryIsReadable(), assertDirectoryIsReadable()
assertDirectoryIsWritable(), assertDirectoryIsWritable()
assertDirectoryNotExists(), assertDirectoryExists()
assertDirectoryNotIsReadable(), assertDirectoryIsReadable()
assertDirectoryNotIsWritable(), assertDirectoryIsWritable()
assertEmpty(), assertEmpty()
assertEquals(), assertEquals()
assertEqualXMLStructure(), assertEqualXMLStructure()
assertFalse(), assertFalse()
assertFileEquals(), assertFileEquals()
assertFileExists(), assertFileExists()
assertFileIsReadable(), assertFileIsReadable()
assertFileIsWritable(), assertFileIsWritable()
assertFileNotEquals(), assertFileEquals()
assertFileNotExists(), assertFileExists()
assertFileNotIsReadable(), assertFileIsReadable()
assertFileNotIsWritable(), assertFileIsWritable()
assertFinite(), assertInfinite()
assertGreaterThan(), assertGreaterThan()
assertGreaterThanOrEqual(), assertGreaterThanOrEqual()
assertInfinite(), assertInfinite()
assertInstanceOf(), assertInstanceOf()
assertInternalType(), assertInternalType()
assertIsReadable(), assertIsReadable()
assertIsWritable(), assertIsWritable()
assertJsonFileEqualsJsonFile(), assertJsonFileEqualsJsonFile()
assertJsonFileNotEqualsJsonFile(), assertJsonFileEqualsJsonFile()
assertJsonStringEqualsJsonFile(), assertJsonStringEqualsJsonFile()
assertJsonStringEqualsJsonString(), assertJsonStringEqualsJsonString()
assertJsonStringNotEqualsJsonFile(), assertJsonStringEqualsJsonFile()
assertJsonStringNotEqualsJsonString(), assertJsonStringEqualsJsonString()
assertLessThan(), assertLessThan()
assertLessThanOrEqual(), assertLessThanOrEqual()
assertNan(), assertNan()
assertNotContains(), assertContains()
assertNotContainsOnly(), assertContainsOnly()
assertNotCount(), assertCount()
assertNotEmpty(), assertEmpty()
assertNotEquals(), assertEquals()
assertNotInstanceOf(), assertInstanceOf()
assertNotInternalType(), assertInternalType()
assertNotIsReadable(), assertIsReadable()
assertNotIsWritable(), assertIsWritable()
assertNotNull(), assertNull()
assertNotRegExp(), assertRegExp()
assertNotSame(), assertSame()
assertNull(), assertNull()
assertObjectHasAttribute(), assertObjectHasAttribute()
assertObjectNotHasAttribute(), assertObjectHasAttribute()
assertPostConditions(), フィクスチャ
assertPreConditions(), フィクスチャ
assertRegExp(), assertRegExp()
assertSame(), assertSame()
assertStringEndsNotWith(), assertStringEndsWith()
assertStringEndsWith(), assertStringEndsWith()
assertStringEqualsFile(), assertStringEqualsFile()
assertStringMatchesFormat(), assertStringMatchesFormat()
assertStringMatchesFormatFile(), assertStringMatchesFormatFile()
assertStringNotEqualsFile(), assertStringEqualsFile()
assertStringNotMatchesFormat(), assertStringMatchesFormat()
assertStringNotMatchesFormatFile(), assertStringMatchesFormatFile()
assertStringStartsNotWith(), assertStringStartsWith()
assertStringStartsWith(), assertStringStartsWith()
assertThat(), assertThat()
assertTrue(), assertTrue()
assertXmlFileEqualsXmlFile(), assertXmlFileEqualsXmlFile()
assertXmlFileNotEqualsXmlFile(), assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile(), assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString(), assertXmlStringEqualsXmlString()
assertXmlStringNotEqualsXmlFile(), assertXmlStringEqualsXmlFile()
assertXmlStringNotEqualsXmlString(), assertXmlStringEqualsXmlString()
attribute(), assertThat()
attributeEqualTo(), assertThat()
Automated Documentation, アジャイルな文書作成

D

Data-Driven Tests, PHPUnit_Framework_Test の実装
Defect Localization, テストの依存性
Depended-On Component, テストダブル
directoryExists(), assertThat()
Documenting Assumptions, アジャイルな文書作成

E

equalTo(), assertThat()
Error (エラー), コマンドラインのテストランナー
Error Handler, PHP のエラーのテスト
Exception, 例外のテスト
expectException(), 例外のテスト
expectExceptionCode(), 例外のテスト
expectExceptionMessage(), 例外のテスト
expectExceptionMessageRegExp(), 例外のテスト
Extreme Programming (エクストリームプログラミング), アジャイルな文書作成

F

Failure (失敗), コマンドラインのテストランナー
fileExists(), assertThat()
Fixture, フィクスチャ
Fluent Interface, スタブ

H

hasAttribute(), assertThat()

I

identicalTo(), assertThat()
include_path, コマンドラインオプション
Incomplete Test (不完全なテスト), 不完全なテスト
isFalse(), assertThat()
isInstanceOf(), assertThat()
isNull(), assertThat()
isReadable(), assertThat()
isTrue(), assertThat()
isType(), assertThat()
isWritable(), assertThat()

L

lessThan(), assertThat()
lessThanOrEqual(), assertThat()
Logfile, コマンドラインオプション
Logging, ログ出力, ログ出力
logicalAnd(), assertThat()
logicalNot(), assertThat()
logicalOr(), assertThat()
logicalXor(), assertThat()

M

matchesRegularExpression(), assertThat()
method(), スタブ
Mock Object, モックオブジェクト

O

onConsecutiveCalls(), スタブ
onNotSuccessfulTest(), フィクスチャ

R

Refactoring (リファクタリング), 開発中のテスト
Report, コマンドラインオプション
returnArgument(), スタブ
returnCallback(), スタブ
returnSelf(), スタブ
returnValueMap(), スタブ

S

setUp(), フィクスチャ
setUpBeforeClass, フィクスチャの共有
setUpBeforeClass(), フィクスチャ
stringContains(), assertThat()
stringEndsWith(), assertThat()
stringStartsWith(), assertThat()
Stub, スタブ
Stubs (スタブ), 複数チームでのテスト
System Under Test, テストダブル

T

tearDown(), フィクスチャ
tearDownAfterClass, フィクスチャの共有
tearDownAfterClass(), フィクスチャ
Template Method, フィクスチャ
Template Method (テンプレートメソッド), フィクスチャ
Test Dependencies, テストの依存性
Test Double, テストダブル
Test Groups, コマンドラインオプション, グループ
Test Isolation, コマンドラインオプション, グローバルな状態
Test Listener, テストリスナー
Test Suite, テストの構成, テストスイート
TestDox, アジャイルな文書作成, @testdox
throwException(), スタブ
timeoutForLargeTests, @large
timeoutForMediumTests, @medium
timeoutForSmallTests, @small

付録E 参考文献

[Astels2003] Test Driven Development. Astels David [FAMILY Given]. 製作著作 © 2003. Prentice Hall. ISBN 0131016490.

[Beck2002] Test Driven Development by Example. Beck Kent [FAMILY Given]. 製作著作 © 2002. Addison-Wesley. ISBN 0-321-14653-0.

[Beck2002-ja] テスト駆動開発入門. Beck Kent [FAMILY Given]. 製作著作 © 2003. ピアソンエデュケーション. ISBN 4894717115.

[Meszaros2007] xUnit Test Patterns: Refactoring Test Code. Meszaros Gerard [FAMILY Given]. 製作著作 © 2007. Addison-Wesley. ISBN 978-0131495050.

付録F 著作権

Copyright (c) 2005-2017 Sebastian Bergmann.

この作品は、Creative Commons Attribution License の下で
ライセンスされています。このライセンスの内容を確認するには、
http://creativecommons.org/licenses/by/2.0/ を訪問するか、あるいは
Creative Commons, 559 Nathan Abbott Way, Stanford, California 943.6,
USA.
に手紙を送ってください。

このライセンスの概要を以下に示します。その後に、完全な文書を示します。

--------------------------------------------------------------------

あなたは以下の条件に従う場合に限り、自由に

    * 本作品を複製、頒布、展示、実演することができます。
    * 二次的著作物を作成することができます。
    * 本作品を営利目的で利用することができます。

あなたの従うべき条件は以下の通りです。

帰属. あなたは原著作者のクレジットを表示しなければなりません。

    * 再利用や頒布にあたっては、この作品の使用許諾条件を他の人々に
      明らかにしなければなりません。

    * 著作[権]者から許可を得ると、これらの条件は適用されません。

上記によってあなたのフェアユースその他の権利が影響を受けることは
まったくありません。

これは、以下に示す完全なライセンスの要約です。

====================================================================

Creative Commons Legal Code
Attribution 3.0 Unported

CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO
WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS
LIABILITY FOR DAMAGES RESULTING FROM ITS USE.

License

THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW
IS PROHIBITED.

BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS
LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU
THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF
SUCH TERMS AND CONDITIONS.

1. Definitions

   a. "Adaptation" means a work based upon the Work, or upon the
      Work and other pre-existing works, such as a translation,
      adaptation, derivative work, arrangement of music or other
      alterations of a literary or artistic work, or phonogram or
      performance and includes cinematographic adaptations or any
      other form in which the Work may be recast, transformed, or
      adapted including in any form recognizably derived from the
      original, except that a work that constitutes a Collection
      will not be considered an Adaptation for the purpose of this
      License. For the avoidance of doubt, where the Work is a
      musical work, performance or phonogram, the synchronization of
      the Work in timed-relation with a moving image ("synching")
      will be considered an Adaptation for the purpose of this
      License.

   b. "Collection" means a collection of literary or artistic works,
      such as encyclopedias and anthologies, or performances,
      phonograms or broadcasts, or other works or subject matter
      other than works listed in Section 1(f) below, which, by
      reason of the selection and arrangement of their contents,
      constitute intellectual creations, in which the Work is
      included in its entirety in unmodified form along with one or
      more other contributions, each constituting separate and
      independent works in themselves, which together are assembled
      into a collective whole. A work that constitutes a Collection
      will not be considered an Adaptation (as defined above) for
      the purposes of this License.

   c. "Distribute" means to make available to the public the
      original and copies of the Work or Adaptation, as appropriate,
      through sale or other transfer of ownership.

   d. "Licensor" means the individual, individuals, entity or
      entities that offer(s) the Work under the terms of this License.

   e. "Original Author" means, in the case of a literary or artistic
      work, the individual, individuals, entity or entities who
      created the Work or if no individual or entity can be
      identified, the publisher; and in addition (i) in the case of
      a performance the actors, singers, musicians, dancers, and
      other persons who act, sing, deliver, declaim, play in,
      interpret or otherwise perform literary or artistic works or
      expressions of folklore; (ii) in the case of a phonogram the
      producer being the person or legal entity who first fixes the
      sounds of a performance or other sounds; and, (iii) in the
      case of broadcasts, the organization that transmits the
      broadcast.

   f. "Work" means the literary and/or artistic work offered under
      the terms of this License including without limitation any
      production in the literary, scientific and artistic domain,
      whatever may be the mode or form of its expression including
      digital form, such as a book, pamphlet and other writing; a
      lecture, address, sermon or other work of the same nature; a
      dramatic or dramatico-musical work; a choreographic work or
      entertainment in dumb show; a musical composition with or
      without words; a cinematographic work to which are assimilated
      works expressed by a process analogous to cinematography; a
      work of drawing, painting, architecture, sculpture, engraving
      or lithography; a photographic work to which are assimilated
      works expressed by a process analogous to photography; a work
      of applied art; an illustration, map, plan, sketch or three-
      dimensional work relative to geography, topography,
      architecture or science; a performance; a broadcast; a
      phonogram; a compilation of data to the extent it is protected
      as a copyrightable work; or a work performed by a variety or
      circus performer to the extent it is not otherwise considered
      a literary or artistic work.

   g. "You" means an individual or entity exercising rights under
      this License who has not previously violated the terms of
      this License with respect to the Work, or who has received
      express permission from the Licensor to exercise rights under
      this License despite a previous violation.

   h. "Publicly Perform" means to perform public recitations of the
      Work and to communicate to the public those public
      recitations, by any means or process, including by wire or
      wireless means or public digital performances; to make
      available to the public Works in such a way that members of
      the public may access these Works from a place and at a place
      individually chosen by them; to perform the Work to the public
      by any means or process and the communication to the public of
      the performances of the Work, including by public digital
      performance; to broadcast and rebroadcast the Work by any
      means including signs, sounds or images.

   i. "Reproduce" means to make copies of the Work by any means
      including without limitation by sound or visual recordings and
      the right of fixation and reproducing fixations of the Work,
      including storage of a protected performance or phonogram in
      digital form or other electronic medium.

2. Fair Dealing Rights. Nothing in this License is intended to
   reduce, limit, or restrict any uses free from copyright or rights
   arising from limitations or exceptions that are provided for in
   connection with the copyright protection under copyright law or
   other applicable laws.

3. License Grant. Subject to the terms and conditions of this
   License, Licensor hereby grants You a worldwide, royalty-free,
   non-exclusive, perpetual (for the duration of the applicable
   copyright) license to exercise the rights in the Work as stated
   below:

   a. to Reproduce the Work, to incorporate the Work into one or
      more Collections, and to Reproduce the Work as incorporated
      in the Collections;

   b. to create and Reproduce Adaptations provided that any such
      Adaptation, including any translation in any medium, takes
      reasonable steps to clearly label, demarcate or otherwise
      identify that changes were made to the original Work. For
      example, a translation could be marked "The original work was
      translated from English to Spanish," or a modification could
      indicate "The original work has been modified.";

   c. to Distribute and Publicly Perform the Work including as
      incorporated in Collections; and,

   d. to Distribute and Publicly Perform Adaptations.

   e. For the avoidance of doubt:

      i. Non-waivable Compulsory License Schemes. In those
         jurisdictions in which the right to collect royalties
         through any statutory or compulsory licensing scheme cannot
         be waived, the Licensor reserves the exclusive right to
         collect such royalties for any exercise by You of the
         rights granted under this License;

      ii. Waivable Compulsory License Schemes. In those
          jurisdictions in which the right to collect royalties
          through any statutory or compulsory licensing scheme can
          be waived, the Licensor waives the exclusive right to
          collect such royalties for any exercise by You of the
          rights granted under this License; and,

      iii. Voluntary License Schemes. The Licensor waives the right
           to collect royalties, whether individually or, in the
           event that the Licensor is a member of a collecting
           society that administers voluntary licensing schemes, via
           that society, from any exercise by You of the rights
           granted under this License.

The above rights may be exercised in all media and formats whether
now known or hereafter devised. The above rights include the right
to make such modifications as are technically necessary to exercise
the rights in other media and formats. Subject to Section 8(f), all
rights not expressly granted by Licensor are hereby reserved.

4. Restrictions. The license granted in Section 3 above is expressly
   made subject to and limited by the following restrictions:

   a. You may Distribute or Publicly Perform the Work only under the
      terms of this License. You must include a copy of, or the
      Uniform Resource Identifier (URI) for, this License with every
      copy of the Work You Distribute or Publicly Perform. You may
      not offer or impose any terms on the Work that restrict the
      terms of this License or the ability of the recipient of the
      Work to exercise the rights granted to that recipient under
      the terms of the License. You may not sublicense the Work. You
      must keep intact all notices that refer to this License and to
      the disclaimer of warranties with every copy of the Work You
      Distribute or Publicly Perform. When You Distribute or
      Publicly Perform the Work, You may not impose any effective
      technological measures on the Work that restrict the ability
      of a recipient of the Work from You to exercise the rights
      granted to that recipient under the terms of the License. This
      Section 4(a) applies to the Work as incorporated in a
      Collection, but this does not require the Collection apart
      from the Work itself to be made subject to the terms of this
      License. If You create a Collection, upon notice from any
      Licensor You must, to the extent practicable, remove from the
      Collection any credit as required by Section 4(b), as
      requested. If You create an Adaptation, upon notice from any
      Licensor You must, to the extent practicable, remove from the
      Adaptation any credit as required by Section 4(b), as requested.

   b. If You Distribute, or Publicly Perform the Work or any
      Adaptations or Collections, You must, unless a request has
      been made pursuant to Section 4(a), keep intact all copyright
      notices for the Work and provide, reasonable to the medium or
      means You are utilizing: (i) the name of the Original Author
      (or pseudonym, if applicable) if supplied, and/or if the
      Original Author and/or Licensor designate another party or
      parties (e.g., a sponsor institute, publishing entity,
      journal) for attribution ("Attribution Parties") in Licensor's
      copyright notice, terms of service or by other reasonable
      means, the name of such party or parties; (ii) the title of
      the Work if supplied; (iii) to the extent reasonably
      practicable, the URI, if any, that Licensor specifies to be
      associated with the Work, unless such URI does not refer to
      the copyright notice or licensing information for the Work;
      and (iv), consistent with Section 3(b), in the case of an
      Adaptation, a credit identifying the use of the Work in the
      Adaptation (e.g., "French translation of the Work by Original
      Author," or "Screenplay based on original Work by Original
      Author"). The credit required by this Section 4 (b) may be
      implemented in any reasonable manner; provided, however, that
      in the case of a Adaptation or Collection, at a minimum such
      credit will appear, if a credit for all contributing authors
      of the Adaptation or Collection appears, then as part of these
      credits and in a manner at least as prominent as the credits
      for the other contributing authors. For the avoidance of
      doubt, You may only use the credit required by this Section
      for the purpose of attribution in the manner set out above
      and, by exercising Your rights under this License, You may not
      implicitly or explicitly assert or imply any connection with,
      sponsorship or endorsement by the Original Author, Licensor
      and/or Attribution Parties, as appropriate, of You or Your use
      of the Work, without the separate, express prior written
      permission of the Original Author, Licensor and/or
      Attribution Parties.

   c. Except as otherwise agreed in writing by the Licensor or as
      may be otherwise permitted by applicable law, if You
      Reproduce, Distribute or Publicly Perform the Work either by
      itself or as part of any Adaptations or Collections, You must
      not distort, mutilate, modify or take other derogatory action
      in relation to the Work which would be prejudicial to the
      Original Author's honor or reputation. Licensor agrees that in
      those jurisdictions (e.g. Japan), in which any exercise of the
      right granted in Section 3(b) of this License (the right to
      make Adaptations) would be deemed to be a distortion,
      mutilation, modification or other derogatory action
      prejudicial to the Original Author's honor and reputation, the
      Licensor will waive or not assert, as appropriate, this
      Section, to the fullest extent permitted by the applicable
      national law, to enable You to reasonably exercise Your right
      under Section 3(b) of this License (right to make Adaptations)
      but not otherwise.

5. Representations, Warranties and Disclaimer

UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING,
LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR
WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED,
STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF
TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE,
NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT
DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF
IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.

6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY
   APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY
   LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE
   OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF
   THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY
   OF SUCH DAMAGES.

7. Termination

   a. This License and the rights granted hereunder will terminate
      automatically upon any breach by You of the terms of this
      License. Individuals or entities who have received Adaptations
      or Collections from You under this License, however, will not
      have their licenses terminated provided such individuals or
      entities remain in full compliance with those licenses.
      Sections 1, 2, 5, 6, 7, and 8 will survive any termination of
      this License.

   b. Subject to the above terms and conditions, the license granted
      here is perpetual (for the duration of the applicable
      copyright in the Work). Notwithstanding the above, Licensor
      reserves the right to release the Work under different license
      terms or to stop distributing the Work at any time; provided,
      however that any such election will not serve to withdraw this
      License (or any other license that has been, or is required to
      be, granted under the terms of this License), and this License
      will continue in full force and effect unless terminated as
      stated above.

8. Miscellaneous

   a. Each time You Distribute or Publicly Perform the Work or a
      Collection, the Licensor offers to the recipient a license to
      the Work on the same terms and conditions as the license
      granted to You under this License.

   b. Each time You Distribute or Publicly Perform an Adaptation,
      Licensor offers to the recipient a license to the original
      Work on the same terms and conditions as the license granted
      to You under this License.

   c. If any provision of this License is invalid or unenforceable
      under applicable law, it shall not affect the validity or
      enforceability of the remainder of the terms of this License,
      and without further action by the parties to this agreement,
      such provision shall be reformed to the minimum extent
      necessary to make such provision valid and enforceable.

   d. No term or provision of this License shall be deemed waived
      and no breach consented to unless such waiver or consent shall
      be in writing and signed by the party to be charged with such
      waiver or consent.

   e. This License constitutes the entire agreement between the
      parties with respect to the Work licensed here. There are no
      understandings, agreements or representations with respect to
      the Work not specified here. Licensor shall not be bound by
      any additional provisions that may appear in any communication
      from You. This License may not be modified without the mutual
      written agreement of the Licensor and You.

   f. The rights granted under, and the subject matter referenced,
      in this License were drafted utilizing the terminology of the
      Berne Convention for the Protection of Literary and Artistic
      Works (as amended on September 28, 1979), the Rome Convention
      of 1961, the WIPO Copyright Treaty of 1996, the WIPO
      Performances and Phonograms Treaty of 1996 and the Universal
      Copyright Convention (as revised on July 24, 1971). These
      rights and subject matter take effect in the relevant
      jurisdiction in which the License terms are sought to be
      enforced according to the corresponding provisions of the
      implementation of those treaty provisions in the applicable
      national law. If the standard suite of rights granted under
      applicable copyright law includes additional rights not
      granted under this License, such additional rights are deemed
      to be included in the License; this License is not intended to
      restrict the license of any rights under applicable law.

Creative Commons is not a party to this License, and makes no
warranty whatsoever in connection with the Work. Creative Commons
will not be liable to You or any party on any legal theory for any
damages whatsoever, including without limitation any general,
special, incidental or consequential damages arising in connection
to this license. Notwithstanding the foregoing two (2) sentences,
if Creative Commons has expressly identified itself as the Licensor
hereunder, it shall have all rights and obligations of Licensor.

Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, Creative Commons does not authorize
the use by either party of the trademark "Creative Commons" or any
related trademark or logo of Creative Commons without the prior
written consent of Creative Commons. Any permitted use will be in
compliance with Creative Commons' then-current trademark usage
guidelines, as may be published on its website or otherwise made
available upon request from time to time. For the avoidance of
doubt, this trademark restriction does not form part of this
License.

Creative Commons may be contacted at http://creativecommons.org/.

====================================================================