Do You PHP?    
Search Engine Optimization  php5 powerd  Valid XHTML 1.0!  Valid CSS!  このサイトのはてなブックマーク数 



last updated
2008/06/20

counter hits
since 1999/11/06


PhpUnit - 最強のユニットテスト自動化ツール

zip形式 tgz形式

memogihyo.jpにPHPUnit3で始めるユニットテストというタイトルで記事を書きました。PHP5をお使いの方は、そちらを参照してください。

alertここにある情報はかなり古くなっており、正しくなくなっている可能性があります。掲載しているサンプルコードiなどは、最新のPHPでは動作しない、もしくは、別途設定・調整が必要になるかも知れません。情報を鵜呑みにせず、あなたの手を動かして、あなたの目で確認してください。

cautionPhpUnitは一時期PEARに登録されていましたが、現在は別プロジェクトとして活動しています。新しいURLはhttp://phpunit.de/です。

テスト。。。ああ、なんてイヤな響きでしょう。。。(^-^;

「テストすること」はプログラムの品質を保証するということで非常に重要な作業だ、ということは百も承知と思いますが、コーディングと比べてやはり「イヤ」なものですねぇ。面倒だし、自分のプログラムの欠点を探すので面白くないし。。。また、リリースしたプログラムの修正が必要かどうか、という状態になったとき、「また再テストかぁ?」ってなりませんか?

「面倒なテストを自動化できたら。。。」ってことを実現するツール、それがPhpUnitです(何か、深夜のTVショッピングのノリだ。。。)。

最近話題になっているXP(Windowsではありません。「eXtreme Programing」と呼ばれる開発手法)で提唱されている12のプラクティス(実践すべき項目)の中で「テスティング」「リファクタリング」で必要となる「自動テストを行うための環境」を構築するためのツールの1つになります。JavaではJUnit、VBではVBUnit、C++ではCppUnitといった具合に、各言語についていろいろなテストツールがあります(Palm Unitってのもあるんですね。。。)。

PhpUnitでカバーできるテストは、あくまで「ユニットテスト」になります。つまり、関数やクラス単位のテストです。今回は、ショッピングカートクラスを対象に考えてみました。

まずは、PhpUnitの入手とインストールです。PhpUnitは、PhpUnit本家(http://sourceforge.net/projects/phpunit/)から入手することができます。2002/03/29現在、最新版はPhpUnit0.3です。PhpUnit本家から入手したtgzは、GNU tarなどで適当なディレクトリ(できれば、include_pathに指定されたディレクトリ)に展開すれば、インストール完了です。

まずは、作成するクラスの仕様を決めます。今回は、ショッピングカートを考えていますので、以下のようなメソッドを定義しました。ここで重要なことは、具体的な実装を書かない事です。

●Cart.phl

<?php
class Cart
{
    /**
     * コンストラクタ
     */
    function Cart()
    {
    }

    /**
     * 商品の追加
     */
    function add($item_cd, $amount)
    {
    }

    /**
     * 特定商品の削除
     */
    function remove($item_cd, $amount)
    {
    }

    /**
     * 全商品のクリア
     */
    function clear()
    {
    }

    /**
     * 特定商品の個数を返す
     */
    function getAmount($item_cd)
    {
    }
}
?>

次にテスト用プログラムを作成します。PhpUnitでは、TestCaseクラス(phpunit.phpファイル内で定義)のサブクラスとして作成します。今回は、add、removeの引数amountに負の数が指定された場合なにもしない、ということにしてテストプログラムを作成しています。

また、テスト用プログラムには、できるだけコメントを書くようにした方が良いようです。まあ、書いておかないと、あとで「これって何のテストやってるんだっけ?」になりそうですしね。

●CartTest.php

<?php
require_once("phpunit.php");
//require_once("PHPUnit.php");    // PEAR版の場合
require_once("Cart.phl");
?>
<?php
/**
 * Cartクラスのテストケース
 * TestCartクラスを継承して作成する
 */
class CartTest extends TestCase
//class CartTest extends PHPUnit_TestCase    // PEAR版の場合
{
    var $cart_;

    /**
     * コンストラクタ
     */
    function CartTest($name)
    {
        // 必ず指定するおまじない
        $this->TestCase($name);
        // $this->PHPUnit_TestCase($name);    // PEAR版の場合
    }

    /**
     * テストの初期化(DB接続などの前処理が必要であれば)
     * テストメソッド毎にsetUp、tearDownが実行される
     */
    function setUp()
    {
        $this->cart_ = new Cart();
    }

    /**
     * テストの終了処理(DB切断などの後処理が必要であれば)
     * テストメソッド毎にsetUp、tearDownが実行される。
     * 今回は使用しない
     */
    function tearDown() {}

    /**
     * 以下実際のテスト
     * 「test」で始まるfunction名がテスト対象となる
     */
    function testInit()
    {
        // assertEqualsメソッドで2つの引数が等価かどうかを調べる。
        // 引数は「期待される値」「実際の値」の順。
        // ここでは、Cartに入っていない商品の個数が0であることを確認。
        $this->assertEquals(0, $this->cart_->getAmount("aaa"));
    }
    function testAddPositive()
    {
        // Cartに商品を追加し、個数が正しいことを確認。
        $this->cart_->add("aaa", 1);
        $this->cart_->add("aaa", 2);
        $this->assertEquals(3, $this->cart_->getAmount("aaa"));

        // 別の商品を追加し、それぞれ個数が正しいことを確認。
        $this->cart_->add("bbb", 10);
        $this->cart_->add("bbb", 20);
        $this->assertEquals(3, $this->cart_->getAmount("aaa"));
        $this->assertEquals(30, $this->cart_->getAmount("bbb"));
    }
    function testAddNegative()
    {
        // 個数に0を指定されたときは、個数に変化がないことを確認。
        $this->cart_->add("aaa", 1);
        $this->cart_->add("aaa", 0);
        $this->cart_->add("bbb", 0);
        $this->cart_->add("bbb", 10);
        $this->assertEquals(1, $this->cart_->getAmount("aaa"));
        $this->assertEquals(10, $this->cart_->getAmount("bbb"));

        // 個数に負数を指定されたときは、何もしない(個数に変化なし)
        // ことを確認。
        $this->cart_->add("aaa", -2);
        $this->cart_->add("aaa", -20);
        $this->assertEquals(1, $this->cart_->getAmount("aaa"));
        $this->assertEquals(10, $this->cart_->getAmount("bbb"));
    }
    function testRemovePositive()
    {
        // 指定された個数分だけ減っていることを複数の商品で確認
        $this->cart_->add("aaa", 10);
        $this->cart_->remove("aaa", 3);
        $this->cart_->add("bbb", 10);
        $this->cart_->remove("bbb", 7);
        $this->assertEquals(7, $this->cart_->getAmount("aaa"));
        $this->assertEquals(3, $this->cart_->getAmount("bbb"));
    }
    function testRemoveNegative()
    {
        // 個数が負数になる場合は、何もしない(個数に変化なし)
        // ことを確認。
        $this->cart_->add("aaa", 10);
        $this->cart_->add("aaa", -20);
        $this->cart_->add("bbb", 1);
        $this->cart_->add("bbb", -2);
        $this->assertEquals(10, $this->cart_->getAmount("aaa"));
        $this->assertEquals(1, $this->cart_->getAmount("bbb"));
    }
    function testClear()
    {
        // 全ての商品の個数が0になることを確認
        $this->cart_->add("aaa", 10);
        $this->cart_->add("bbb", 5);
        $this->cart_->clear();
        $this->assertEquals(0, $this->cart_->getAmount("aaa"));
        $this->assertEquals(0, $this->cart_->getAmount("bbb"));
    }
}

/**
 * 全てのテストを実行
 * TestSuiteクラスの引数には、TestCaseクラスのサブクラス名を指定する
 */
$ts = new TestSuite("CartTest");
$tr = new TestRunner();
$tr->run($ts);

/**
 * 以下、PEAR版の場合
 */
//$ts = new TestSuite("CartTest");
//$tr = PHPUnit::run($ts);
//echo '<pre>';
//echo preg_replace("/( failed:.*)/", "<font color=\"red\"><b>\${1}</b></font>", $tr->toString());
//echo '</pre>';
?>

テストプログラムを作成したらブラウザからアクセスし、テストに失敗することを確認しておきます。これは、正しい実装がないとエラーになることを確認するためです。実行結果はこんな感じです。

テストプログラムの作成・確認が終わったら、Cartクラスの実装を行います。以下がこの時点でのコードで、全てのテストをパスしています。実際には、コーディングとテストを交互に行い、最終的に全てのテストをパスするまでこれを繰り返しました

実行結果はこうなります

●Cart.php

<?php
class Cart
{
    var $items_;

    /**
     * コンストラクタ
     */
    function Cart()
    {
        $this->items_ = array();
    }

    /**
     * 商品の追加
     */
    function add($item_cd, $amount)
    {
        if (!isset($this->items_[$item_cd])) {
            $this->items_[$item_cd] = 0;
        }
        if ($amount > 0) {
            $this->items_[$item_cd] += $amount;
        }
    }

    /**
     * 特定商品の削除
     */
    function remove($item_cd, $amount)
    {
        if (!isset($this->items_[$item_cd])) {
            $this->items_[$item_cd] = 0;
        }
        if ($amount > 0) {
            $this->items_[$item_cd] -= $amount;
            if ($this->items_[$item_cd] < 0) {
                $this->items_[$item_cd] = 0;
            }
        }
    }

    /**
     * 全商品のクリア
     */
    function clear()
    {
        unset($this->items_);
    }

    /**
     * 特定商品の個数を返す
     */
    function getAmount($item_cd)
    {
        if (!isset($this->items_[$item_cd])) {
            $this->items_[$item_cd] = 0;
        }
        return $this->items_[$item_cd];
    }

}
?>

ここで、コードを修正してみます。add、remove、getAmountの各メソッドに同じコードがありますので、これを_checkというprivateメソッドにまとめ、それそれ呼び出すことにします。以下が修正後のコードです。

●Cart.php

<?php
class Cart
{
    var $items_;

    /**
     * コンストラクタ
     */
    function Cart()
    {
        $this->items_ = array();
    }

    /**
     * 商品の追加
     */
    function add($item_cd, $amount)
    {
        $this->_check($item_cd);
        if ($amount > 0) {
            $this->items_[$item_cd] += $amount;
        }
    }

    /**
     * 特定商品の削除
     */
    function remove($item_cd, $amount)
    {
        $this->_check($item_cd);
        if ($amount > 0) {
            $this->items_[$item_cd] -= $amount;
            if ($this->items_[$item_cd] < 0) {
                $this->items_[$item_cd] = 0;
            }
        }
    }

    /**
     * 全商品のクリア
     */
    function clear()
    {
        unset($this->items_);
    }

    /**
     * 特定商品の個数を返す
     */
    function getAmount($item_cd)
    {
        $this->_check($item_cd);
        return $this->items_[$item_cd];
    }

    /**
     * 商品個数のチェック&初期化
     */
    function _check($item_cd)
    {
        if (!isset($this->items_[$item_cd])) {
            $this->items_[$item_cd] = 0;
        }
    }
}
?>

修正が終わったら、またテストします。全てのテストにパスすれば、めでたく修正完了となります。上のコードは全てのテストにパスしています。

テストが自動化されていますので、今までのような「修正をするとテストが面倒」ということはないですね。API(メソッドのシグネチャ)を変えない限り、全く違う実装でも全てのテストをパスする限り、動作が保証されます。これって、開発者にとって非常に強力な(楽するための)味方ではないでしょうか?



About This Site |  Privacy Policy |  Contact
Copyright © 1999 - 2008 by Hideyuki SHIMOOKA all rights reserved.