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

第3回 ショッピングカートクラスを作ってみる(2)

この記事を読むのに必要な時間:およそ 18 分

Cartクラスとテストケースのリファクタリング

最後の修正でaddメソッドのネストが若干深くなってしまいましたので,ここでaddメソッドのリファクタリングをしておきましょう。

テストが自動化されていない場合,一般的に一度テストしたコードを変更するには「勇気」が必要となります。なぜなら,再度すべてのテストを行う必要があるからです。これには多くの時間と手間がかかります。また,品質の低下にも気を付ける必要があります。しかし,これまで様々なテストを作成し,自動的にテストを行えるようにしてきました。実行にはそれほどの時間も手間もかかりません。また,すべてのテストをパスする限り,品質の維持を保証することができます。このことは開発者にとって,非常に大きな助けになります。

前置きがちょっと長くなりましたが,以下がaddメソッド内のif文を組み替えたコードになります。

<?php
class Cart
{
    private $items;

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

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

    public function add($item_cd, $amount) {
        if (!preg_match('/^-?\d+$/', $amount)) {
            throw new UnexpectedValueException('Invalid amount');
        }

        if (!isset($this->items[$item_cd])) {
            $this->items[$item_cd] = 0;
        }
        $this->items[$item_cd] += (int)$amount;

        if ($this->items[$item_cd] > PHP_INT_MAX) {
            throw new OutOfRangeException('the amount exceeded PHP_INT_MAX');
        }
        if ($this->items[$item_cd] <= 0) {
            unset($this->items[$item_cd]);
        }

        return true;
    }

    public function getAmount($item_cd) {
        if (isset($this->items[$item_cd])) {
            return $this->items[$item_cd];
        } else {
            return 0;
        }
    }

    public function clear() {
        $this->items = array();
    }
}

リファクタリング後,テストを実行して問題がないことを確認します。

$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

..........

Time: 0 seconds


OK (10 tests)
$ 

ついでに,テストケースもリファクタリングしておきましょう。

これまで作成したすべてのテストでは,最初にCartクラスのインスタンス化を行っていますが,それぞれのテストを開始する前に,自動的にCartクラスのインスタンス化を行えると,スッキリしそうです。PHPUnitでは,こういう場面で使用できるsetUpメソッドが用意されています。また,setUpメソッドと対となり,テスト終了時に自動的にコールされるtearDownメソッドも用意されています。

メソッド意味
void setUp()これをオーバーライドして,実行するテストに関連するオブジェクトの作成を行います。テストケース内で各テストが実行されるたびに,setUp() が毎回コールされます。
void tearDown()これをオーバーライドして,実行するテストに関連する,もう必要なくなったオブジェクトの後始末を行います。テストケース内で各テストが実行されるたびに,setUp() が毎回コールされます。一般に,tearDown() で明示的に後始末する必要があるのは外部リソース (例えばファイルやソケットなど) だけです。

それでは,setUpメソッドを使ってテストケースを書き直してみます。

<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';
class CartTest extends PHPUnit_Framework_TestCase
{
    protected $cart;

    protected function setUp() {
        $this->cart = new Cart();
    }

    public function testInitCart() {
        $this->assertTrue(is_array($this->cart->getItems()));
        $this->assertEquals(0, count($this->cart->getItems()));
    }

    public function testAdd() {
        $this->assertTrue($this->cart->add('001', 1));
        $this->assertTrue($this->cart->add('001', 0));
        $this->assertTrue($this->cart->add('001', -1));
    }

    public function testAddNotNumeric() {
        try {
            $this->cart->add('001', 'string');
        } catch (UnexpectedValueException $e) {
            return;
        }
        $this->fail();
    }

    public function testAddFloat() {
        try {
            $this->cart->add('001', 1.5);
        } catch (UnexpectedValueException $e) {
            return;
        }
        $this->fail();
    }

    public function testGetAmount() {
        $this->assertEquals(0, $this->cart->getAmount('001'));
        $this->cart->add('001', 1);
        $this->assertEquals(1, $this->cart->getAmount('001'));

        $this->assertEquals(0, $this->cart->getAmount('999'));

        $this->cart->add('002', 1);
        $this->assertEquals(1, $this->cart->getAmount('001'));
        $this->assertEquals(1, $this->cart->getAmount('002'));

        $this->cart->add('001', -1);
        $this->assertEquals(0, $this->cart->getAmount('001'));
        $this->assertEquals(1, $this->cart->getAmount('002'));
    }

    public function testGetItems() {
        $this->cart->add('001', 3);
        $this->assertEquals(1, count($this->cart->getItems()));
        $this->cart->add('002', 2);
        $this->assertEquals(2, count($this->cart->getItems()));

        $items = $this->cart->getItems();
        $this->assertEquals(3, $items['001']);
        $this->assertEquals(2, $items['002']);
    }

    public function testClearCart() {
        $this->cart->add('001', 1);
        $this->cart->add('002', 2);
        $this->cart->add('003', 3);
        $this->cart->clear();
        $this->assertTrue(is_array($this->cart->getItems()));
        $this->assertEquals(0, count($this->cart->getItems()));
    }

    public function testAddUpperLimit() {
        $this->cart->add('001', PHP_INT_MAX);
        try {
            $this->cart->add('001', 1);
        } catch (OutOfRangeException $e) {
            return;
        }
        $this->fail();
    }

    public function testAddUnderLimit() {
        $this->cart->add('001', 1);
        $this->assertEquals(1, count($this->cart->getItems()));
        $this->cart->add('001', -1);
        $this->assertEquals(0, count($this->cart->getItems()));

        $this->cart->clear();
        $this->cart->add('001', 1);
        $this->assertEquals(1, count($this->cart->getItems()));
        $this->cart->add('001', -2);
        $this->assertEquals(0, count($this->cart->getItems()));
    }

    public function testAddRemove() {
        $this->cart->add('001', 2);
        $this->cart->add('002', 3);
        $this->assertEquals(2, count($this->cart->getItems()));

        $this->cart->add('001', -2);
        $items = $this->cart->getItems();
        $this->assertEquals(1, count($items));
        $this->assertFalse(isset($items['001']));
        $this->assertEquals(3, $items['002']);

        $this->cart->add('002', -3);
        $items = $this->cart->getItems();
        $this->assertEquals(0, count($items));
        $this->assertFalse(isset($items['001']));
        $this->assertFalse(isset($items['002']));
    }
}

phpunitコマンドを実行して問題がないことを確認します。

$ phpunit CartTest
 PHPUnit 3.1.7 by Sebastian Bergmann.

..........

Time: 0 seconds


OK (10 tests)
$ 

これで完成です。

次回は,ダミーのオブジェクト(モックオブジェクト)を使ったテストについて解説・実践していきます。

著者プロフィール

下岡秀幸(しもおかひでゆき)

PHP関連の情報サイト「Do You PHP?」の管理人。PHP歴は長いが,あまり仕事で使ったことがないという噂がある。最近はα版のPEAR・PECLに手を出しては地雷を踏んでいることが多い。

URLhttp://www.doyouphp.jp/http://d.hatena.ne.jp/shimooka/