PHPUnit3で始めるユニットテスト

第1回PHPUnit入門

はじめに

皆さん、テストしてますか?

近年、システム開発を発注する顧客や利用ユーザーの品質に対する要求レベルは格段に向上しています。そのため、システムの品質を保証するための「テストフェーズ」はますます欠かせなくなってきています。

ここで、一口に「テスト」といっても、フェーズによって以下のような様々なテストがあります。

  • ユニットテスト・単体テスト
  • 結合テスト・システムテスト
  • 総合テスト
  • 受け入れテスト
  • 負荷テスト
  • セキュリティテスト

筆者の経験上、一般的なシステム開発でもっとも大きな工数を占めるのは、この「テスト」フェーズと考えています。なぜなら、テストフェーズは例外なく「繰り返し作業」だからです。前述の様々なテストフェーズで共通することですが、テストフェーズは「テストで発見されたバグ・障害を修正して再度テストを行う」という作業を何度も繰り返し行うフェーズです。あとのフェーズで不具合が発見された場合、当然プログラムを修正することになりますが、それに伴ってそれより前のフェーズのテストも再度行う必要が発生します。つまり、発見されるバグ・障害の数やフェーズに従って、作業量(=工数)が指数関数的に増加する、ということを意味しています。このため、製造フェーズの早い段階で問題点をあぶり出しておくことが重要になってきます。

また、システムの「開発」よりも「保守」の方がずっと長期かつ重要であることを開発者は忘れがちです。システム開発を発注する顧客にとっては、⁠システムが完成したら終わり」ではありません。やっとスタートラインに立っただけなのです。その保守フェーズでは、システムの改修や拡張、仕様追加・変更などが発生します。そのたびにプログラムを修正しテストを行う必要がありますが、機能のデグレードや思わぬ不具合が発生する可能性があります。このように、システムの品質を「保証」し続ける事は、非常に重要なことです。

さらに、開発者としては「間違いなくこれで問題ありません」と自信を持ってリリースできれば精神的にも良いのですが、⁠これは本当にリリースしても大丈夫なのか?」⁠他に影響ないだろうな?」といった不安を抱えたままリリース作業に突入してしまい・あとは言わずもがなですね。

近年では、XP(eXtreme Programming)やTDD(Test Driven Development)といった反復型の開発手法がだいぶ一般的になってきていますが、それらの手法の重要な要素の1つに「テストの自動化」が挙げられています。それらが採用される理由は多々ありますが、システムの品質を保ちつつ、開発や改修・拡張を行っていける手法であることが大きな要因ではないかと思います。

テストを自動化することで、人手をかけずいつでも自動的にテストできるようになります。また、コードのデグレードや問題点などを事前に検知することもできます。結果、コードの品質を一定に保つことが可能になります。また、開発者の不安を取り除く、精神的にも大きな助けとなります。つまり、自信を持って改修、リリースできるようになるということです。これは直接的な効果ではありませんが、開発者に非常に大きな影響を与えます。また、⁠テスト」という必須と分かっているけれども開発者にとって(どちらかというと)ネガティブな作業を、⁠コーディング」という開発者にとってポジティブな作業に変える側面も持っていると考えています。

さて、XPやTDDで行う「テストの自動化」は、xUnitと呼ばれるテスティングフレームワークを利用しています。これらxUnitは名前の通り、ユニットテストをターゲットとしたテスティングフレームワークです。実際にxUnitというものが存在するわけではなく、⁠x」の部分にJavaの場合は「J⁠⁠、VBの場合は「VB⁠⁠、C++の場合は「Cpp」という具合に言語名の略称が入り、それらの総称として「xUnit」が使われます。PHPの場合、今回見ていく「PHPUnit」があります。

PHPUnitとは

すでによく知っている・実際に使っている方も多いと思いますが、改めてPHPUnitをご紹介します。PHPUnitとは、Sebastian Bergmann氏が開発を行っているテスティングフレームワークの1つです。

PHPUnit - Trac

画像

2007年8月15日現在、バージョン3.1.7がリリースされており、PHP5.1.4以降で利用可能です。PHPUnitのpear.php.net/package/PHPUnit">バージョン1バージョン2PEARサイトで管理されていますが、すでにメンテナンスは終了しています。また、バージョン3以降、Sebastian氏のサイトで管理されるようになっています。なお、バージョン2.3.0β1からPHP5専用となっています。

最新系列のPHPUnit3.1は非常に多機能です。以下、主な機能を上げておきます。

  • コマンドラインのテストランナー
  • TestSuiteのサポート
  • 拡張TestCase
  • モックオブジェクトのサポート
  • コードカバレッジ解析(Xdebugが必須)
  • 様々なフォーマットでのログ出力(XML、JSON、Test Anything Protocol (TAP)、GraphViz)
  • テストコードの雛形ジェネレータ

本連載では、PHPUnit3の基本的な使い方やプロジェクトで役立つ機能について解説していきます。

使用する環境

本連載の動作確認に使用した環境は、CentOS4.5+Apache2.2.4+PHP5.2.3+PHPUnit3.1.7です。PHPは以下のようなconfigureオプションを付けてソースからbuildしています。

--with-apxs2=/usr/local/apache2/bin/apxs \
--prefix=/usr/local/lib/php5 \
--with-pear=/usr/local/lib/php5/pear \
--with-config-file-path=/usr/local/lib/php5/ini/5.2.3 \
--with-config-file-scan-dir=/usr/local/lib/php5/ini.d \
--enable-zend-multibyte \
--enable-mbstring=shared \
--with-dom \
--enable-filter=shared \
--with-gettext=shared \
--with-mcrypt=shared \
--with-gd=shared \
--with-jpeg-dir \
--with-png-dir \
--with-zlib-dir \
--with-ttf \
--with-freetype-dir \
--enable-gd-native-ttf \
--enable-gd-jis-conv \
--enable-soap=shared \
--enable-pdo=shared \
--with-pdo-sqlite=shared \
--with-sqlite=shared \
--enable-sqlite-utf8 \
--with-openssl=shared \
--with-curl=shared \
--enable-bcmath=shared \
--enable-pcntl=shared \
--enable-sockets=shared \
--with-oci8=shared,/usr/lib/oracle/xe/app/oracle/product/10.2.0/server/ \
--with-pdo-oci=shared,/usr/lib/oracle/xe/app/oracle/product/10.2.0/server/ \
--with-pdo=shared \
--enable-sigchild \
--with-xsl=shared \
--enable-zip=shared

「--prefix」オプションを付けているため、phpコマンドやpearコマンド、その他コマンドのパスがデフォルトとは変わっていますので、適宜読み替えてください。また、便宜上、作業ユーザーでPEARライブラリなどをインストールできるよう、sudoerファイルを編集してあります。

PHPUnitのインストール

PHPUnitのサイトに掲載されている手順でインストールします。pearコマンドを使用する場合は、次のようになります。

$ sudo pear channel-discover pear.phpunit.de
$ sudo pear install -a phpunit/PHPUnit

インストール後、PEARインストールディレクトリ直下のbinディレクトリにphpunitというコマンドファイルが存在することを確認し、動作を確認しておきましょう。

$ export PATH=$PATH:/usr/local/lib/php5/bin/
$ which phpunit
/usr/local/lib/php5/bin/phpunit
$ ls /usr/local/lib/php5/bin/phpunit
/usr/local/lib/php5/bin/phpunit
$ phpunit --help
PHPUnit 3.1.7 by Sebastian Bergmann.

Usage: phpunit [switches] UnitTest [UnitTest.php]

  --log-graphviz <file>  Log test execution in GraphViz markup.
  --log-json <file>      Log test execution in JSON format.
  --log-tap <file>       Log test execution in TAP format to file.
  --log-xml <file>       Log test execution in XML format to file.

  --test-db-dsn <dsn>    DSN for the test database.
  --test-db-log-rev <r>  Revision information for database logging.
  --test-db-log-info ... Additional information for database logging.

  --testdox-html <file>  Write agile documentation in HTML format to file.
  --testdox-text <file>  Write agile documentation in Text format to file.

  --filter <pattern>     Filter which tests to run.
  --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.

  --no-syntax-check      Disable syntax check of test source files.
  --stop-on-failure      Stop execution upon first error or failure.
  --verbose              Output more verbose information.
  --wait                 Waits for a keystroke after each test.

  --skeleton             Generate skeleton UnitTest class for Unit in Unit.php.
  --help                 Prints this usage information.
  --version              Prints the version and exits.

  --d key[=value]         Sets a php.ini value.
$ 

次は、PHPUnitの基本的な使い方を見ていきましょう。

PHPUnitの基本的な使い方

xUnitはテスティング「フレームワーク」と呼ばれるだけあって、お決まりのルールがあり、それに従ってテスト用のクラスを作成していきます。一般的に、それらのテスト用クラスは「テストケース」と呼ばれます。PHPUnitの場合、基本的には、用意されているPHPUnit_Framework_TestCaseクラスを継承してサブクラスを作成します。そして、⁠test」で始まるpublicメソッド、もしくはメソッドのコメントにアノテーション「@test」を付けたメソッドに具体的なテスト内容を記述していきます。また、ファイル名は

クラス名.php

とします。以下は、PHPUnitポケットガイドに記載されているサンプルを元にしたもので、ファイル名はArrayTest.phpです。

<?php
require_once 'PHPUnit/Framework.php';
 
class ArrayTest extends PHPUnit_Framework_TestCase
{
    public function testNewArrayIsEmpty()
    {
        // 配列を作成します。
        $fixture = array();

        // 配列のサイズは 0 です。
        $this->assertEquals(0, sizeof($fixture));
    }

    /**
     * メソッド名がtestで始まっていないが、「@test」
     * アノテーションを使用していることに注意
     *
     * @test
     */
    public function arrayContainsAnElementTest()
    {
        // 配列を作成します。
        $fixture = array();
 
        // 配列にひとつの要素を追加します。
        $fixture[] = 'Element';
 
        // 配列のサイズは 1 です。
        $this->assertEquals(1, sizeof($fixture));
    }
}

ここでは2つのテストメソッド

  • testNewArrayIsEmpty
  • testArrayContainsAnElement

が用意されていますが、それぞれのメソッドでは、

  1. テストしたいコードを実行
  2. assertEqualsメソッドを実行

といった処理を実行していることが分かると思います。2番目のassertEqualsメソッドでテストした結果を検証しているメソッドで、2つの値が一致しているかどうかを検証します。

assertEqualsの他にもassertXXXXメソッドが多く用意されています。以下に主なassertXXXXメソッドを挙げておきます。その他のassertXXXXメソッドについては、PHPUnitポケットガイドを参照してください。

アサーション意味
void assertTrue(bool $condition)$condition が FALSE の場合にエラーを報告します。
void assertTrue(bool $condition, string $message)$condition が FALSE の場合にエラー $message を報告します。
void assertFalse(bool $condition)$condition が TRUE の場合にエラーを報告します。
void assertFalse(bool $condition, string $message)$condition が TRUE の場合にエラー $message を報告します。
void assertNull(mixed $variable)$variable が NULL でないときにエラーを報告します。
void assertNull(mixed $variable, string $message)$variable が NULL でないときにエラー $message を報告します。
void assertNotNull(mixed $variable)$variable が NULL の場合にエラーを報告します。
void assertNotNull(mixed $variable, string $message)$variable が NULL の場合にエラー $message を報告します。
void assertEquals(string $expected, string $actual)2 つの文字列 $expected と $actual が等しくない場合にエラーを報告します。エラーは、2 つの文字列の差分で報告されます。
void assertEquals(string $expected, string $actual, string $message)2 つの文字列 $expected と $actual が等しくない場合にエラー $message を報告します。エラーは、2 つの文字列の差分で報告されます。
void assertNotEquals(string $expected, string $actual)2 つの文字列 $expected と $actual が等しい場合にエラーを報告します。
void assertNotEquals(string $expected, string $actual, string $message)2 つの文字列 $expected と $actual が等しい場合にエラー $message を報告します。
void assertEquals(mixed $expected, mixed $actual)2 つの変数 $expected と $actual が等しくない場合にエラーを報告します。
void assertEquals(mixed $expected, mixed $actual, string $message)2 つの変数 $expected と $actual が等しくない場合にエラー $message を報告します。
void assertNotEquals(mixed $expected, mixed $actual)2 つの変数 $expected と $actual が等しい場合にエラーを報告します。
void assertNotEquals(mixed $expected, mixed $actual, string $message)2 つの変数 $expected と $actual が等しい場合にエラー $message を報告します。

それでは、ここでテストを実行してみましょう。テストケースのファイル名が[クラス名].phpの場合、phpunitコマンドにテストケースのクラス名を指定して実行します。

$ phpunit ArrayTest
PHPUnit 3.1.7 by Sebastian Bergmann.

..

Time: 0 seconds


OK (2 tests)
$ 

また、テストケースのファイル名が[クラス名].phpでない場合、phpunitコマンドにテストケースのクラス名とファイル名を両方を指定します。

$ phpunit ArrayTest TestSample.php
PHPUnit 3.1.7 by Sebastian Bergmann.

..

Time: 0 seconds


OK (2 tests)
$ 

テスト結果を見ると「.」が2つ表示されていますが、1つ1つの「.」がテストが成功したことを表しています。失敗した場合、以下のように「F」と表示されます。

$ phpunit ArrayTest
PHPUnit 3.1.7 by Sebastian Bergmann.

F.

Time: 0 seconds

There was 1 failure:

1) testNewArrayIsEmpty(ArrayTest)
Failed asserting that <integer:0> matches expected value <integer:1>.
/home/shimooka/public_html/gihyo.jp/01.phpunit/04/ArrayTest.php:12

FAILURES!
Tests: 2, Failures: 1.
$ 

テストの実行結果をまとめると、以下のようになります。

表示意味
.テストが成功した
Fテストに失敗した
Eテストの実行中、エラーが発生した
Sテストがスキップされた
Iテストが「不完全」あるいは「未実装」とマークされているため、実行しなかった

実際にテストを書く際のポイント

先の例は、PHP組み込みの配列やsizeof関数ですので、最初から機能が完成されています。また、テストケースもすでに用意されていました。XPやTDDを採用している場合、そもそもテスト対象を実装する前にテストケースを作成しますので、このパターンはまずあり得ません。また、開発後にテストケースを作成する場合もありますが、実際にはなかなかうまくいかないものです。そこで、実際にテストケースを書く際のポイントを挙げてみます。

テスト対象の仕様を明確にしておく

テストケースを書くためには、仕様を明確にしておく必要があります。なぜなら、⁠どういうテストをすればよいか」を決めるためには、⁠それがどのように動作するか」を決めておく必要があるためです。つまり、⁠テストを書くために、仕様を明確にする」とも言えます。また、不足している仕様があぶり出される、という効果もあります。

最初にテストが失敗することを確認しておく

先にテストを作成する場合、テスト対象の実装前にテストが失敗する事を確認しておきましょう。実装後、テストが成功したとしても、最初からテストが成功していたのであれば、その結果は疑わしいものになってしまいます。必ず、⁠テストに失敗していたが、実装することでテストに成功するようになった」と言えるようでなければいけません。

「まずは」テストをパスさせ、テストと実装を交互に繰り返す

実際には、テストケースやテスト対象のクラス・関数を、最初から完全に実装することは難しい場合が多いです。気負わずにまずはテストをパスするだけのシンプルな実装から始めましょう。そして、⁠テスト作成/実装/テスト」を繰り返し、実装をブラッシュアップさせます。そうすることで、最終的な実装もシンプルになります。

テスト対象の大きさや責務

テスト対象が大きくなったり多くの機能を持っている場合、テストケースが書きにくくなったり複雑になったりしてしまいます。明確にする仕様の「粒度」にも気を配りましょう。XPやTDDなどテストが優先される開発手法の場合、テストケースの粒度に合わせて実装が行われますので、自然と適切な粒度になる場合が多くなります。

次回から数回にわたって、実際にPHPUnitを使ってテストを行いつつ、ショッピングカートクラスを実装していきます。

おすすめ記事

記事・ニュース一覧