テストを書かないのが許されるのは小学生まで
テスト書いてますか?ボクはボチボチ書いてます。
TDD(Test Driven Development)なんて言葉がメジャーになって久しいわけですが、ボクもRisolutoの開発を始めてから少しずつテスト書くようにしてます。まあ、どうやって書けば良いか分からない部分とかもあるので、できるところから少しずつ……って感じですが。
さて、「テストを書く」といえば必ず話題に出てくるのが「nUnit」系のアレ。PHPの場合は、PHPUnitですね。
このエントリでは、Risolutoの開発を通して得たノウハウを元に、Composerでテスト環境を作る方法をまとめていますよ。
Composerのセットアップ
まずComposerを使えるようにしなきゃね!PHPはすでにインストールされてることが前提だよ!
……ってことで、適当なディレクトリ(*1)を用意します。その中に「composer.json」ってファイルを作ります。そのファイルの中身はこんな感じで書いておきます。
{ "require": { "phpunit/phpunit": "4.1.*", "phpunit/php-file-iterator": "1.3.*", "phpunit/php-text-template": "1.2.*", "phpunit/php-code-coverage": "2.0.*", "phpunit/php-timer": "1.0.*", "phpunit/phpunit-mock-objects": "2.1.*", "phpunit/dbunit": "1.3.*" } }
まあ、PHPUnit関連のあれこれをインストールしますよって指定をずらずらと書いてるだけです。いらなそうなのもあるけど後で必要になって入れるのがメンドイのでまとめて入れてます。
コロンで区切られた後ろのほうはインストールするバージョンの指定なんだけども、このブログ執筆時点でいい感じに動くものを指定しているので、場合によっては修正しないと古くさくて使えないってことがあるかも。
作成したらコマンドプロンプトから「/path/to/dir」に移動して、「curl -sS https://getcomposer.org/installer | php」とタイプします。しばらく待ちます。終わったら続いて「php composer.phar install -o」とタイプします。しばらく待ちます。
$ cd /path/to/dir $ curl -sS https://getcomposer.org/installer | php #!/usr/bin/env php All settings correct for using Composer Downloading... Composer successfully installed to: /Users/haya/test/composer.phar Use it: php composer.phar $ php composer.phar install -o Loading composer repositories with package information Installing dependencies (including require-dev) - Installing sebastian/exporter (1.0.1) Loading from cache - Installing sebastian/diff (1.1.0) Loading from cache - Installing sebastian/comparator (1.0.0) Loading from cache - Installing phpunit/php-text-template (1.2.0) Loading from cache - Installing phpunit/phpunit-mock-objects (2.1.5) Loading from cache - Installing sebastian/version (1.0.3) Loading from cache - Installing sebastian/environment (1.0.0) Loading from cache - Installing phpunit/php-token-stream (1.2.2) Loading from cache - Installing phpunit/php-file-iterator (1.3.4) Loading from cache - Installing phpunit/php-code-coverage (2.0.10) Loading from cache - Installing phpunit/php-timer (1.0.5) Loading from cache - Installing symfony/yaml (v2.5.3) Loading from cache - Installing phpunit/phpunit (4.1.6) Loading from cache - Installing phpunit/dbunit (1.3.1) Loading from cache phpunit/phpunit suggests installing phpunit/php-invoker (~1.1) Writing lock file Generating optimized autoload files
画面にエラーっぽいものがでてなければ、「/path/to/dir」のなかにいろいろできているはず。
テストの準備
続いてテストを走らせる準備をしませぅ。ここからは各プロダクト毎に細かい調整が必要かも。Risolutoの場合をベースに書いていきます。あくまで例示目的なので、ここに書いた内容はそのままだと上手く動かないかもしれないにょ。
テスト実行用のシェルスクリプト
ボクはテストを簡単に実行できるようにシェルスクリプトを作ってます。「/path/to/dir/phpunit_run.sh」って名前で下記のような内容を書いてマス。
#!/bin/bash /path/to/dir/vendor/bin/phpunit --bootstrap=/path/to/dir/phpunit_bootstrap.php --configuration=/path/to/dir/phpunit.xml --verbose --colors
一応解説しておくと下記のような感じ。
- 「/path/to/dir/vendor/bin/phpunit」ってのがPHPUnitのCLIコマンドなのでそれを実行するようにしている
- Composerのオートローダを使ったりしたいので、「/path/to/dir/phpunit_bootstrap.php」をブートローダに指定している
- テストの設定は「/path/to/dir/phpunit.xml」ってファイルにまとめかいている
- 「--verbose」で詳しい情報が出力されるようにしている
- 「--colors」で出力メッセージをカラフルにしている
ブートストラップ
続いてブートストラップである「/path/to/dir/phpunit_bootstrap.php」の中身について。RisolutoではComposerのオートローダを使ったりしているので、そういったのをテストの実行前に用意する為に使っていますよ。
<?php $autoloader = '/path/to/dir/vendor/autoload.php'; clearstatcache(true); if (file_exists($autoloader) and is_file($autoloader) and is_readable($autoloader)) { // オートローダが存在すれば読み込む require_once($autoloader); } else { // 存在しなければ強制終了 die('autoloader.php was not found'); }
簡単に言えば、そのテストの対象となるコードが必要としているものを用意するのがコレ。特に必要なければ作らなくてもOK。
テスト設定ファイル
続いてテストの設定を書いてある「/path/to/dir/phpunit.xml」。テストに関する設定をまとめて書いてる感じ。
<phpunit> <php> <var name="DB_DSN" value="mysql:host=127.0.0.1;port=3306;dbname=example;charset=utf8"/> <var name="DB_USER" value="root"/> <var name="DB_PASSWORD" value=""/> <var name="DB_DBNAME" value="example_table"/> </php> <testsuites> <testsuite name="SuiteName"> <file>/path/to/dir/normaltest.pht</file> <file>/path/to/dir/dbtest.pht</file> </testsuite> </testsuites> </phpunit>
最初の「<php>〜</php>」の部分はPHPで使える変数なんかを定義するのに使う。この例ではDBへ接続する際に必要となるじょうほうを定義していて、テストコード中では「$GLOBALS['DB_DSN']」みたいに参照できる。スーパーグローバルで渡ってくるのは気持ち悪い気もするけど。
「<testsuites>〜</testsuites>」のところがテストコードについての設定。その内側にある「<testsuite>〜</testsuite>」ってところでひとかたまり毎にテストコードをまとめている感じ。この「testsuite」単位でテストを実行できる(*2)ので、それを見越して分割しておくとよさげ。で、さらにその内側にある「<file>〜</file>」ってところに実際のテストコードが書かれたPHPファイルを指定するのです。
たとえば、「testsuite」はクラス単位で分割して、「file」はメソッド単位で分割……みたいにすると管理しやすかったりするかもしれない。そこはお好みで。
DB定義
DB接続を伴うテストをしたい場合はDBの定義を用意しておく。「/path/to/dir/phpunit_db.xml」的な名前で下記のような内容を用意しておく。
<?xml version="1.0" ?> <dataset> <table name="example_table"> <column>column1</column> <column>column2</column> <row> <value>row1_column1</value> <value>row1_column2</value> </row> <row> <value>row2_column1</value> <null /> </row> </table> </dataset>
これは「example_table」という名称のテーブルでカラムが2つあり、データとして2行用意するという定義。ちなみにDBやテーブルは勝手に作られることはないので、「CREATE DATABASE/TABLE」にてテスト実行前に用意しておく必要がある。これは事前に用意したそれを初期化する際の定義。
テストコードを書こう!
ここまで準備ができたら、後はテストコードを書くべし。
普通のテスト
たとえば下記のような内容を書く。「/path/to/dir/normaltest.pht」とかの名前で保存しておく(*3)。
<?php class normalTest extends PHPUnit_Framework_TestCase { protected function setUp() { // テスト前に何かやっておきたいことがあればここで書いておく } public function testFooBar() { $this->assertTrue(true); } }
ルールは簡単。「PHPUnit_Framework_TestCase」を継承すること。
テスト時に何か事前準備が必要なら「setUp()」メソッドに書くこと、テストは「test」から始まるメソッドに書くこと。そして、「$this->assert*」ってメソッド(*4)でテスト結果の判定(アサーション)を行うこと。
DB接続が必要なテスト
DB接続が必要な場合は一手間増える。「/path/to/dir/dbtest.pht」とかの名前で保存しておく(*3)。
<?php class dbTest extends PHPUnit_Extensions_Database_TestCase { protected function setUp() { // テスト前に何かやっておきたいことがあればここで書いておく // DB周りの初期化を行う為に元々のsetUp()をコールする parent::setUp(); } public function getConnection() { $pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWORD']); return $this->createDefaultDBConnection($pdo, $GLOBALS['DB_DBNAME']); } public function getDataSet() { return $this->createXMLDataSet('/path/to/dir/phpunit_db.xml'); } public function testExample() { $this->assertEquals(2, $this->getConnection()->getRowCount('risoluto_db_test')); } }
普通のテストと違うところだけまとめておく。
「PHPUnit_Framework_TestCase」でなく「PHPUnit_Extensions_Database_TestCase」を継承すること。
「setUp()」メソッドでテーブルの初期化が行われる。ので、「setUp()」メソッドをオーバーライドしたら、かならず元の「setUp()」メソッドをコールすること。コレをしないと最悪死ぬ。
「getConnection()」メソッドを用意すること。テーブルの初期化やアサーションによっては必要となるDB接続を提供しており、「/path/to/dir/phpunit.xml」で定義された情報を使用している。
「getDataSet()」メソッドも用意すること。テーブルを初期化する内容を提供するメソッドで「/path/to/dir/phpunit_db.xml」の内容に基づいて行われる。
あとDB接続が絡む場合には特殊なアサーションが使える(*5)ことも覚えておこう。
テストを実行しよう
テストを実行するには「/path/to/dir/phpunit_run.sh」を実行するだけ。下記はRisolutoのテストを実行した場合だけど、テストを実行すると下記みたいな出力が得られる。Risolutoにはいくつか不完全なテストが含まれているので「Incomplete」がでてるけどテスト自体は正常に終了しているはず。
PHPUnit 4.1.6 by Sebastian Bergmann. Configuration read from /path/to/dir/risoluto/lib/phpunit.xml ........I...................................................... 63 / 151 ( 41%) ..........................III...........................I...... 126 / 151 ( 83%) ......................... Time: 1.61 seconds, Memory: 8.50Mb There were 5 incomplete tests: 1) RisolutoCoreTest::testDummy We have no idea for this test... I need your help... /path/to/dir/risoluto/lib/vendor_test/Risoluto/CoreTest/CoreTest.php:52 2) RisolutoFileTest::testDummy We have no idea for this test... I need your help... /path/to/dir/risoluto/lib/vendor_test/Risoluto/FileTest/FileTest.php:52 3) RisolutoLogTest::testDummy We have no idea for this test... I need your help... /path/to/dir/risoluto/lib/vendor_test/Risoluto/LogTest/LogTest.php:52 4) RisolutoSessionTest::testDummy We have no idea for this test... I need your help... /path/to/dir/risoluto/lib/vendor_test/Risoluto/SessionTest/SessionTest.php:52 5) RisolutoUrlTest4RedirectTo::test_RedirectTo We have no idea for this test... I need your help... /path/to/dir/risoluto/lib/vendor_test/Risoluto/UrlTest/UrlTest4RedirectTo.php:42 OK, but incomplete, skipped, or risky tests! Tests: 151, Assertions: 384, Incomplete: 5.
上手く動かない場合は、出力されたメッセージやログをチェックしたり各種ファイルを確認するようにしよう。もう一度言うけど、このエントリではRisolutoのブツをベースに雰囲気が分かる程度にアレしたものなので、そのままでは上手く動かないかもしれない。
分からないことがあったら……
まずオフィシャルのドキュメントに目を通そう。
それでも分からないって場合があると思うけど、その時はググろう。場合によっては既存のプロダクトに含まれているテストコードを読んでみよう。FLOSS(OSS)系なら不十分だとしてもテストコードがあるはず。
それらを自分で読んでも分からなければ、思い切ってテストコードを書いた人に聞いてみるって手もある(*6)。ただし、相手のことも考えよう。
*1:仮に「/path/to/dir」としますぜ
*2:「phpunit --testsuite {テストスイート名のパターン}」でいけるはず
*3:「/path/to/dir/phpunit.xml」で定義したファイル名に合わせること
*4:付録A アサーションに使えるメソッドが列挙されているので目を通しておくといい
*5:データベースアサーション APIを参照
*6:ボクに聞いてくれても良いけど深いことは分かってないよ?