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

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

ショッピングカートクラスを作ってみる

これから、実際にPHPUnitを使ってテストケースを書きつつ、クラスの実装を行ってみましょう。今回作成するのは、以下の機能を持つ簡易的なショッピングカートクラス(Cartクラス)です。

  • 前提条件:商品コード、数量共に自然数とする
  1. 初期状態では、カートに入れられた商品数は0
  2. 商品を追加する場合、商品コードと数量(正数)を指定する
  3. 数量を変更する場合、商品コードと変更する数量(整数)を指定する
  4. 商品コードを指定して、カートに入っている数量を取得できる
  5. すべての商品コードと数量をまとめて取得できる
  6. クリアすると、カートは初期状態になる

1番目の仕様「初期状態では、カートに入れられた商品数は0」

それでは早速、1番目の仕様である

初期状態では、カートに入れられた商品数は0

を実装していきましょう。まずは、テストケースから作成しましょう。Cartクラスのテストケースなので、テストケースのクラス名をCartTest、ファイル名をCartTest.phpとします。

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

class CartTest extends PHPUnit_Framework_TestCase
{
}

それではphpunitコマンドを実行してみます。

$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

F

Time: 0 seconds

There was 1 failure:

1) Warning(PHPUnit_Framework_Warning)
No tests found in class "CartTest".

FAILURES!
Tests: 1, Failures: 1.
$ 

実行するテストがまだ存在しないため、失敗してしまいます。それでは、1つ目の仕様に対するテストを書いてみます。ただし、1つ目の仕様だけでは「どうやって初期状態の商品数が0であることをテストするか?」が書きにくいと思います。そこで、5番目の仕様

すべての商品コードと数量をまとめて取得できる

と併せてテストを書いていくことにします。また、5番目の仕様で実装する予定のメソッドをgetItemsメソッドとし、getItemsメソッドは商品コードをキー、カートに入れられた商品の数を値とするハッシュを返すものとしました。

<?php
require_once 'PHPUnit/Framework.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()));
    }
}

そして、再びphpunitコマンドを実行します。

$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

PHP Fatal error:  Class 'Cart' not found in /home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php on line 7

Fatal error: Class 'Cart' not found in /home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php on line 7
$ 

今度はCartクラスが存在しないためのFatal errorとなりました。必要最低限のCartクラスとメソッドの枠だけ作成し(Cart.php⁠⁠、テストケースで読み込むようにしましょう。

<?php
class Cart
{
    public function 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()));
    }
}

そしてphpunitコマンドの実行です。

$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

F

Time: 0 seconds

There was 1 failure:

1) testInitCart(CartTest)
Failed asserting that <boolean:false> is true.
/home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php:9

FAILURES!
Tests: 1, Failures: 1.

$this->assertTrueメソッドでfalseが返ってきているためのエラーのようです。まだCartクラスのgetItemsメソッドを実装していたいためなので当然ですね。では、getItemsメソッドを実装しましょう。

<?php
class Cart
{
    public function getItems() {
        return array();
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

.

Time: 0 seconds


OK (1 test)
$ 

「えっ?」と思われたかも知れませんが、まずはテストが通るようになりました。ひとまず、これで実装完了としておきましょう。

2番目の仕様「商品を追加する場合、商品コードと数量(正数)を指定する」

次に、2番目の仕様

商品を追加する場合、商品コードと数量(正数)を指定する

です。この仕様に対するテストはどのようなものでしょうか?ここではまず以下の2つを考えてみます。

  • 追加するためのメソッドにコードと正数を渡すとtrueを返す
  • 追加するためのメソッドにコードと非正数(0と負の正数)を渡すと、例外を投げる

なお、投げられる例外は、SPL(Standard PHP Library)拡張モジュールで定義されている例外OutOfBoundsExceptionとします。それでは、テストケースにテストを追加します。

<?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 testAddPositive() {
        $cart = new Cart();
        $this->assertTrue($cart->add('001', 1));
    }

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

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

ここで例外のテストについて若干説明します。PHPUnit3では例外をテストする方法として次の2つが用意されています。

  • PHPUnit_Extensions_ExceptionTestCaseを使用する
  • try/catch構文とfailメソッドを使用する

先のテストケースでは、testAddZeroメソッドとtestAddNegativeメソッドで例外のテストを行っており、2番目の方法を採っています。testXXXXメソッド内で使われているfailメソッドは「ボトルネックメソッド」と呼ばれ、テストが失敗した際PHPUnitによってコールされるメソッドです。テストに失敗してtry/catch文を通過した場合に、このfailメソッドを直接呼び出すことで「テストに失敗した」ことをPHPUnitに伝えています。また、階層構造を持った例外をテストする場合、意図した例外を正しく捕捉しているかどうか、十分注意してください。なお、PHPUnit_Extensions_ExceptionTestCaseの解説については、PHPUnitポケットガイドを参照してください。

それでは、テストを実行しましょう。

$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

.PHP Fatal error:  Call to undefined method Cart::add() in /home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php on line 15

Fatal error: Call to undefined method Cart::add() in /home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php on line 15
$ 

Fatal errorが発生しましたので、Cartクラスにaddメソッドを実装します。

<?php
class Cart
{
    public function getItems() {
        return array();
    }

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

.FFF

Time: 0 seconds

There were 3 failures:

1) testAddPositive(CartTest)
Failed asserting that <null> is true.
/home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php:15

2) testAddZero(CartTest)
/home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php:25

3) testAddNegative(CartTest)
/home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php:35

FAILURES!
Tests: 4, Failures: 3.
$ 

新しいテストが正しく失敗する(変な表現ですが)ことを確認しましたので、addメソッドの実装を行いましょう。

<?php
class Cart
{
    public function getItems() {
        return array();
    }

    public function add($item_cd, $amount) {
        if ((int)$amount > 0) {
            return true;
        } else {
            throw new OutOfBoundsException('Invalid amount');
        }
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

....

Time: 0 seconds


OK (4 tests)
$ 

うまくいきました。しかし、これで本当に「正数」だけうまく通ると言えるでしょうか?数量に小数や文字列を渡された場合のテストも追加して確認しておきましょう。

<?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 testAddPositive() {
        $cart = new Cart();
        $this->assertTrue($cart->add('001', 1));
    }

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

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

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

    public function testAddFloat() {
        $cart = new Cart();
        try {
            $cart->add('001', 1.5);
        } catch (OutOfBoundsException $e) {
            return;
        }
        $this->fail();
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

.....F

Time: 0 seconds

There was 1 failure:

1) testAddFloat(CartTest)
/home/shimooka/public_html/gihyo.jp/01.phpunit/CartTest.php:55

FAILURES!
Tests: 6, Failures: 1.
$ 

小数のテストで失敗しました。addメソッドを修正し、数量のフォーマットをチェックすることにします。

<?php
class Cart
{
    public function getItems() {
        return array();
    }

    public function add($item_cd, $amount) {
        if (preg_match('/^\d+$/', $amount) && (int)$amount > 0) {
            return true;
        } else {
            throw new OutOfBoundsException('Invalid amount');
        }
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

......

Time: 0 seconds


OK (6 tests)
$ 

今度は大丈夫そうですね。これで2番目の仕様の実装完了としましょう。

3番目の仕様「数量を変更する場合、商品コードと変更する数量(整数)を指定する」

続いて、3番目の仕様

数量を変更する場合、商品コードと変更する数量(整数)を指定する

です。よく考えてみると、2番目の仕様で実装した内容と今回実装しようとしている内容は「商品コードと数量を指定してカートの中身を変更する」ことに変わりありません。1つのメソッドに統一できそうですね。今回はaddメソッドを変更することで、3番目の仕様を実装する事にしましょう。それでは今までと同様、仕様に対するテストを考えてみます。

  • 変更するためのメソッドにコードと任意の整数を渡すと、trueを返す
  • 変更するためのメソッドにコードと非数を渡すと、例外UnexpectedValueException((OutOfBoundsExceptionと同様、SPLで定義されている例外))を投げる

これがaddメソッドの新しい仕様となります。まずは、addメソッドの実装で用意したテストを修正しましょう。

<?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();
    }
}

随分スッキリしてしまいました。それでは、phpunitコマンドを実行して、テスト結果を確認しておきましょう。

$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

.EEE

Time: 0 seconds

There were 3 errors:

1) testAdd(CartTest)
OutOfBoundsException: Invalid amount
/home/shimooka/public_html/gihyo.jp/CartTest.php:16

2) testAddNotNumeric(CartTest)
OutOfBoundsException: Invalid amount
/home/shimooka/public_html/gihyo.jp/CartTest.php:23

3) testAddFloat(CartTest)
OutOfBoundsException: Invalid amount
/home/shimooka/public_html/gihyo.jp/CartTest.php:33

FAILURES!
Tests: 4, Errors: 3.
$ 

3箇所でエラーが発生するようになりました。16行目は、数量に0を指定できるかどうかのテストで、今回仕様変更になった箇所です。23行目と33行目は投げられる例外が変更になったことが原因です。それでは、Cartクラスを変更します。

<?php
class Cart
{
    public function getItems() {
        return array();
    }

    public function add($item_cd, $amount) {
        if (preg_match('/^\d+$/', $amount) && (int)$amount >= 0) {
            return true;
        } else {
            throw new UnexpectedValueException('Invalid amount');
        }
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

.E..

Time: 0 seconds

There was 1 error:

1) testAdd(CartTest)
UnexpectedValueException: Invalid amount
/home/shimooka/public_html/gihyo.jp/CartTest.php:17

FAILURES!
Tests: 4, Errors: 1.
$ 

今度はCartTest.phpの17行目にある数量に負数を指定した場合のテストで失敗しています。同様に修正します。

<?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');
        }
    }
}
$ phpunit CartTest
PHPUnit 3.1.7 by Sebastian Bergmann.

....

Time: 0 seconds


OK (4 tests)
$ 

テストに通りました。こちらも随分コードがスッキリしましたね。これで3番目の仕様の実装も完了としましょう。

次回は、残りの3つの仕様をテストを作りながら実装していきます。

おすすめ記事

記事・ニュース一覧