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

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

前回に引き続き、残りの仕様を実装していきましょう。

4番目の仕様「商品コードを指定して、カートに入っている数量を取得できる」

次は4番目の仕様

商品コードを指定して、カートに入っている数量を取得できる

です。ここでは、以下の3つのテストを考えてみました。

  • Cartオブジェクトを作成した直後は0を返す
  • 商品コード「001」を1個追加すると、getAmount('001')は1を返す
  • カートに入っていない商品が指定された場合、数量として0を返す

テストを追加したテストケースは次のようになりました。

<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';

class CartTest extends PHPUnit_Framework_TestCase
{
    public function testInitCart() {
        $cart = new Cart();
        $this->assertTrue(is_array($cart->getItems()));
        $this->assertEquals(0, count($cart->getItems()));
    }

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

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

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

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

        $this->assertEquals(0, $cart->getAmount('999'));
    }
}
<?php
class Cart
{
    public function getItems() {
        return array();
    }

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

    public function getAmount($item_cd) {
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

....F

Time: 0 seconds

There was 1 failure:

1) testGetAmount(CartTest)
Failed asserting that <null> matches expected value <integer:0>.
/home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php:43

FAILURES!
Tests: 5, Failures: 1.
$ 

さて、それではgetAmountメソッドを実装していきます。もうお気づきの方も多いと思いますが、現時点で商品情報をCartクラス内に保持する実装をしていません。テスト内容も貯まってきていますので、そろそろ本格的に実装し始めても良い頃でしょう。

まず、1番目の仕様に対するテストを実装した際、Cartクラス内の商品情報は、キーに商品コード、値に数量を持つハッシュでとすることにしましたので、それを実装しましょう。また、addメソッドも併せて修正しておきます。修正したCartクラスは以下のようになりました。

<?php
class Cart
{
    private $items;

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

    public function getItems() {
        return array();
    }

    public function add($item_cd, $amount) {
        if (preg_match('/^-?\d+$/', $amount)) {
            if (!isset($this->items[$item_cd])) {
                $this->items[$item_cd] = 0;
            }
            $this->items[$item_cd] += (int)$amount;
            return true;
        } else {
            throw new UnexpectedValueException('Invalid amount');
        }
    }

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

テストを実行してみましょう。

$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

.....

Time: 0 seconds


OK (5 tests)
$ 

今までのテストを含め、全てテストをパスしています。しかし、1つの商品コードに対してのテストしか行っていません。複数の商品の場合の不具合はないのでしょうか?それもテストしてみましょう。以下のテストケースは、1つのカートに複数の商品を追加・数量変更し、それぞれの個数が正しく返ってくるかどうかのテストを追加しています。

<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';

class CartTest extends PHPUnit_Framework_TestCase
{
    public function testInitCart() {
        $cart = new Cart();
        $this->assertTrue(is_array($cart->getItems()));
        $this->assertEquals(0, count($cart->getItems()));
    }

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

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

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

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

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

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

        $cart->add('001', -1);
        $this->assertEquals(0, $cart->getAmount('001'));
        $this->assertEquals(1, $cart->getAmount('002'));
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

.....

Time: 0 seconds


OK (5 tests)
$ 

大丈夫そうです。これで4番目の仕様も実装完了とします。

5番目の仕様「すべての商品コードと数量をまとめて取得できる」

1番目の仕様を実装する際に追加したgetItemsメソッドですが、テストをまだ追加していませんでしたので、テストを追加しておきましょう。ここでは、以下の2つのテストを考えてみました。

  • カートに複数の商品を追加した場合の商品種の数
  • getItemsメソッドで取得したハッシュの内容のチェック

テストを追加したテストケースは次のようになりました。

<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';

class CartTest extends PHPUnit_Framework_TestCase
{
    public function testInitCart() {
        $cart = new Cart();
        $this->assertTrue(is_array($cart->getItems()));
        $this->assertEquals(0, count($cart->getItems()));
    }

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

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

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

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

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

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

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

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

        $items = $cart->getItems();
        $this->assertEquals(3, $items['001']);
        $this->assertEquals(2, $items['002']);
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

.....F

Time: 0 seconds

There was 1 failure:

1) testGetItems(CartTest)
Failed asserting that <integer:0> matches expected value <integer:1>.
/home/shimooka/public_html/gihyo.jp/CartTest.php:60

FAILURES!
Tests: 6, Failures: 1.
$ 

おっと。getItemsメソッドは、1つ目の仕様を実装した際の仮実装のままでした。ここで、Cartクラスを修正して再度テストします。

<?php
class Cart
{
    private $items;

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

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

    public function add($item_cd, $amount) {
        if (preg_match('/^-?\d+$/', $amount)) {
            if (!isset($this->items[$item_cd])) {
                $this->items[$item_cd] = 0;
            }
            $this->items[$item_cd] += (int)$amount;
            return true;
        } else {
            throw new UnexpectedValueException('Invalid amount');
        }
    }

    public function getAmount($item_cd) {
        if (isset($this->items[$item_cd])) {
            return $this->items[$item_cd];
        } else {
            return 0;
        }
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

......

Time: 0 seconds


OK (6 tests)
$ 

6番目の仕様「クリアすると、カートは初期状態になる」

次は最後の仕様

クリアすると、カートは初期状態になる

です。仕様に対するテストはどの様になるでしょうか?とりあえず、

  • clearメソッドを呼び出すと、コンストラクタを呼び出した場合と同様の動作をする

ということで良さそうです。早速テストケースにテストを追加し、Cartクラスにclearメソッドの定義を追加しましょう。

<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';

class CartTest extends PHPUnit_Framework_TestCase
{
    public function testInitCart() {
        $cart = new Cart();
        $this->assertTrue(is_array($cart->getItems()));
        $this->assertEquals(0, count($cart->getItems()));
    }

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

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

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

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

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

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

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

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

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

    public function testClearCart() {
        $cart = new Cart();
        $cart->add('001', 1);
        $cart->add('002', 2);
        $cart->add('003', 3);
        $cart->clear();
        $this->assertTrue(is_array($cart->getItems()));
        $this->assertEquals(0, count($cart->getItems()));
    }
}
<?php
class Cart
{
    private $items;

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

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

    public function add($item_cd, $amount) {
        if (preg_match('/^-?\d+$/', $amount)) {
            if (!isset($this->items[$item_cd])) {
                $this->items[$item_cd] = 0;
            }
            $this->items[$item_cd] += (int)$amount;
            return true;
        } else {
            throw new UnexpectedValueException('Invalid amount');
        }
    }

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

    public function clear() {
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

......F

Time: 0 seconds

There was 1 failure:

1) testClearCart(CartTest)
Failed asserting that <integer:3> matches expected value <integer:0>.
/home/shimooka/public_html/gihyo.jp/CartTest.php:76

FAILURES!
Tests: 7, Failures: 1.
$ 

それでは、clearメソッドを実装していきます。clearメソッドの動作はコンストラクタと同様の動作をすれば良いので、コンストラクタのコード内容をそのままコピー&ペーストして実装することにします。

<?php
class Cart
{
    private $items;

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

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

    public function add($item_cd, $amount) {
        if (preg_match('/^-?\d+$/', $amount)) {
            if (!isset($this->items[$item_cd])) {
                $this->items[$item_cd] = 0;
            }
            $this->items[$item_cd] += (int)$amount;
            return true;
        } else {
            throw new UnexpectedValueException('Invalid amount');
        }
    }

    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 (7 tests)
$ 

テストにパスしました。⁠これで実装完了」といきたいところですが、重複するコードは可能な限り避けたいものです。そこでコンストラクタのコードを変更し、clearメソッドを呼び出すことにしましょう。

<?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)) {
            if (!isset($this->items[$item_cd])) {
                $this->items[$item_cd] = 0;
            }
            $this->items[$item_cd] += (int)$amount;
            return true;
        } else {
            throw new UnexpectedValueException('Invalid amount');
        }
    }

    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 (7 tests)
$ 

テスト結果も問題ないようです。

不足している仕様とテストを追加する

さて、挙げられた仕様はすべて実装しましたが、ここで不足している仕様やテストがないかどうかを確認してみましょう。まず、カートに入っている商品の数量に制限がありません。数量が負数になることはないと考えられますので、下限は0としましょう。一方、数量の上限はシステムの要件で異なることが多いですので、ちょっと非現実的ですが、ここではPHP_INT_MAX(2147483647)としておきましょう。また、数量が上限・下限を越えた場合の処理はどうでしょうか?ここでは、上限を超えた場合はSPL拡張モジュールで定義されているOutOfRangeExceptionを投げ、下限を越えた場合はエラーは発生せず、数量が0になることにしましょう。まとめると、以下の3つを新しい仕様として追加します。

  • カートに入っている商品の数量は0以上PHP_INT_MAX以下
  • 数量の上限を超過した場合、OutOfRangeExceptionを投げる
  • 数量の下限を超過した場合、数量は0になる

現在の仕様では「数量が0」である商品もカートの中に存在することができてしまいます。この場合、カートから商品を取り除いた方が良いと考えられますので、これも仕様として追加しておきましょう。これにより、上の3つ目の仕様は

  • 数量が下限もしくは下限を超過した場合、その商品はカートから取り除かれる

となります。

それでは、新しく挙げた仕様を実装します。前者2つはaddメソッドのテストとして、3つ目はgetItemsメソッドのテストとして、それぞれテストケースに追加することにします。

<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';

class CartTest extends PHPUnit_Framework_TestCase
{
    public function testInitCart() {
        $cart = new Cart();
        $this->assertTrue(is_array($cart->getItems()));
        $this->assertEquals(0, count($cart->getItems()));
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        $cart->add('002', -3);
        $items = $cart->getItems();
        $this->assertEquals(0, count($items));
        $this->assertFalse(isset($items['001']));
        $this->assertFalse(isset($items['002']));
    }
}
$ phpunit CartTest
  PHPUnit 3.1.7 by Sebastian Bergmann.

.......FFF

Time: 0 seconds

There were 3 failures:

1) testAddUpperLimit(CartTest)
/home/shimooka/public_html/gihyo.jp/CartTest.php:87

2) testAddUnderLimit(CartTest)
Failed asserting that <integer:1> matches expected value <integer:0>.
/home/shimooka/public_html/gihyo.jp/CartTest.php:95

3) testAddRemove(CartTest)
Failed asserting that <integer:2> matches expected value <integer:1>.
/home/shimooka/public_html/gihyo.jp/CartTest.php:112

FAILURES!
Tests: 10, Failures: 3.
$ 

テストが通るように、Cartクラスを実装していきます。

<?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)) {
            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;
        } else {
            throw new UnexpectedValueException('Invalid amount');
        }
    }

    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
PHP Parse error:  syntax error, unexpected T_PUBLIC in /home/shimooka/public_ht ml/gihyo.jp/Cart.php on line 29

Parse error: syntax error, unexpected T_PUBLIC in /home/shimooka/public_html/gi hyo.jp/Cart.php on line 29
[shimooka@centos gihyo.jp]$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

..........

Time: 0 seconds


OK (10 tests)
$ 

うまくいきました。

テストのコードカバー率を確認する

さて、Cartクラスも少しずつ大きなコードになってきていますが、すべてのコードが実行されているのでしょうか?ここで、実行されたコードのカバー率(コードカバレッジ)を確認してみましょう。PHPUnit3はPHP拡張モジュールであるXdebugと組み合わせることで、コードのカバー率を確認することができます。XdebugはPECLパッケージとして提供されていますので、インストールは以下の手順で行うことができます。

$ su -
# pecl install -a xdebug
# echo "zend_extension=/usr/local/lib/php5/lib/php/extensions/no-debug-non-zts-20060613/xdebug.so" >> /usr/local/lib/php5/ini/5.2.3/php.ini
#

インストール後、正しくインストールされているかどうか確認します。

$ php -i | grep -i "xdebug support"

xdebug support => enabled
# 

また、phpunitコマンドのヘルプを表示すると、追加オプションである「--coverage-xml」⁠--report」が使用可能になっていることが分かります。

$ 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.

  --coverage-xml <file>  Write code coverage information in XML format.
  --report <dir>         Generate combined test/coverage report in HTML format.
  --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.
$ 

Xdebugを正しくインストールできたら、早速コードカバー率の結果を出力させてみましょう。

$ phpunit --report ./report CartTest
 PHPUnit 3.1.7 by Sebastian Bergmann.

..........

Time: 0 seconds


OK (10 tests)

Generating report, this may take a moment.sh: neato: command not found

$ 

実行後、カレントディレクトリ直下にreportディレクトリが作成され、HTMLファイルやCSSファイルが生成されていることを確認してください。また、index.htmlをブラウザで開くと次のように表示されていると思います。

画像

また、⁠Cart.php」のリンクをクリックすると、行単位の結果が表示されます。

画像

これらの結果を見ると、すべてのコードが実行されており、コードカバー率は充分であると判断できます。これでCartクラスもほぼ完成です。

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)
$ 

これで完成です。

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

おすすめ記事

記事・ニュース一覧