Help us understand the problem. What is going on with this article?

CakePHP + PHPUnitでの TDD 超入門

More than 5 years have passed since last update.

はじめに

このエントリーは TDD AdventCalendar 2013 の第21目の記事です。
前日のエントリーは、tq_jappyさんによる「レガシーテストコード改善ガイド #TddAdventJp」でした。

わたしも Advent Calendar 初参加です。どうぞよろしくお願いします。


この記事では主に TDD とはなにか聞いたことはあるけど、実践したことはない、というような方を対象に、
CakePHP と PHPUnit を使った TDD の進め方をご紹介したいと思います。
CakePHP と PHPUnit はそれぞれ開発環境にインストールされて、使える状態になっていることを仮定します。

準備

まずは CakePHP インストール直後の、この画面が出るところから始めます。

CakePHP__the_rapid_development_php_framework__Home-3.png

何をテストするか

今回はテスト対象として、ソーシャルゲームでよくあるような処理を取り上げてみたいと思います。
テスト対象として、 User モデルのパラメータ hp を回復させるメソッド を考えます。

データベースの準備

CakePHP でテストを実行するには通常使う DB の他に、テスト専用のDBを用意する必要があります。
これを作成して、app/Config/database.php に情報を追加しておきましょう。
今回は通常使う DB を advent, テスト専用の DB を test_advent という名前で作成しました。

database.php
<?php
class DATABASE_CONFIG {

    public $default = array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'root',
        'password' => '',
        'database' => 'advent',
        'prefix' => '',
        'encoding' => 'utf8'
    );

    public $test = array(
        'datasource' => 'Database/Mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'root',
        'password' => '',
        'database' => 'test_advent',
        'prefix' => '',
        'encoding' => 'utf8'
    );
}

テーブルの作成

User モデルは users テーブルを使用し、そのスキーマは以下のとおりとします。

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `hp` int(11) NOT NULL DEFAULT '0',
  `mhp` int(11) NOT NULL DEFAULT '0',
  `modified` datetime DEFAULT NULL,
  `created` datetime DEFAULT NULL,
  `deleted` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |

id, 名前 の他にパラメータとして hp (体力) と mhp (体力の最大値)を持っているという簡単なモデルです。

モデルの作成

テーブルができたら、モデルクラスを作ります。ここでは bake コマンドを使って作成してみます。
プロジェクトルートにて、

sh app/Console/cake bake model

と打つとモデルを作成する bake スクリプトが起動します。
bake_model.png

作成可能なモデルとして User モデルが候補1.に挙がっているので"1"を入力してエンターを押します。

すると、バリデーションやリレーションを自動的に作成するかの質問に答えた後(ここでは両方とも NO としました。)
モデルのクラスファイル app/Model/User.php とともに、
テストファイル app/Test/Case/Model/UserTest.php と
フィクスチャファイル app/Test/Fixture/UserFixture.php が生成されます。

---------------------------------------------------------------
The following Model will be created:
---------------------------------------------------------------
Name:       User
DB Table:   `advent`.`users`
---------------------------------------------------------------
Look okay? (y/n)
[y] > y

Baking model class for User...

Creating file /mnt/AdventCalendar/app/Model/User.php
Wrote `/mnt/AdventCalendar/app/Model/User.php`

Baking test fixture for User...

Creating file /mnt/AdventCalendar/app/Test/Fixture/UserFixture.php
Wrote `/mnt/AdventCalendar/app/Test/Fixture/UserFixture.php`
Bake is detecting possible fixtures...

Baking test case for User Model ...

Creating file /mnt/AdventCalendar/app/Test/Case/Model/UserTest.php
Wrote `/mnt/AdventCalendar/app/Test/Case/Model/UserTest.php`

まだ具体的なメソッドやテストケースなど何も実装していませんが、ともかくこれで最初のユニットテストを実行する準備ができました。

ユニットテストを実行してみる

ブラウザから実行

CakePHP にはブラウザからユニットテストを実行できるテストランナーが最初から設定されていて大変手軽にTDDが始められます(すてき!)まずはこれを使っていきましょう。
ブラウザで webroot 直下の test.php にアクセスしてみると以下の様な画面が表示されます。

test_top.png

先ほど作成した User モデルのテストが実行できるようになっているのでさっそくリンクを押してみると…

user_test_1.jpg

FAILED の赤い文字とともに、テストケースがないのでテストが失敗した旨が表示されます。まだ何も実装していないので当然ですね。

TDD してみる

おめでとうございます!これでようやく TDD を始める準備が整いました。
TDD の "赤 → 緑 → リファクタリング" サイクルの最初は赤、つまりテストが失敗するところから始まるのでしたね。

ここから、 TDD の手法に則って 冒頭に述べたユーザーの hp を回復させるメソッドを開発してみます。

メソッドの仕様

テストを書くにも何が正しいのが決まっていないと書けませんから、まずはメソッドの仕様を決めましょう。
今回は、

ユーザーモデルの配列を渡すとそれの hp を最大値(= mhp)と等しい値にして、またユーザーモデルの配列として返す」

というものにしたいと思います。メソッド名とシグネチャーは

public function recoverHp($user);

としましょう。

テストケースの作成

TDDの世界では テストファースト が重要なのでした。仕様が決まれば次にやるべきことはメソッドの実装ではなく、テストケースの作成です!bake によって作成された UserTest.php にテストケースを追加しましょう。

bake によって setUp() や tearDown() のような PHPUnit の基本的なメソッドがすでに作成されていますので、その下に自分のテストケースを追加します。

UserTest.php
    public function testRecoverHp() {
        // テストデータを準備
        $user = [
            'User' => [
                'id'   => 1,
                'name' => 'みなみ',
                'hp'   => 3,
                'mhp'  => 100,
            ],
        ];

        // 期待する結果を記述
        $expected = [
            'User' => [
                'id'   => 1,
                'name' => 'みなみ',
                'hp'   => 100, // hp が回復した!
                'mhp'  => 100,
            ],
        ];

        // テスト対象メソッドを呼び出す
        $result = $this->User->recoverHp($user);

        // 期待される結果が得られたか?
        $this->assertEquals($expected, $result);
    }

テスト実行(REDフェーズ)

ここでもう一度テストを実行してみます。

user_test_2.jpg

メッセージは変わりましたが、依然として赤(テスト失敗)のままです。
そもそもテスト対象のメソッドをまだ実装していませんから当然ですね。
(メソッドが見つかりません、のようなエラーではないものが出ているのは、
CakePHP のモデルでは実装されていないメソッドを呼び出すと
そのメソッド名を SQL として DB に問い合わせる、というおかしな仕様があるためです。)

メソッド実装(GREENフェーズ)

さて、失敗するテストケースを作成できたので、次にやるべきはこれを最速で成功させることです。
User モデルにテスト対象のメソッドを 秒速で 実装しましょう。

User.php
    public function recoverHp($user) {
        return [
            'User' => [
                'id'   => 1,
                'name' => 'みなみ',
                'hp'   => 100,
                'mhp'  => 100,
            ]
        ];
    }

そして再びテストを実行すると・・・!

user_test_3.jpg

やたー!テストが成功して緑のバーが表示されました。
これでひとまず、あるひとつのテストケースにおいては仕様を満たすメソッドを実装できたことになります。

リファクタリング

しかし安心するにはまだ早い。上の実装は入力値の如何にかかわらず常に固定の値を返しています。
これではみなみさん以外のユーザーには対応できませんし、
みなみさんの mhp が上昇した場合などにも対応できていません。
つまり入力値によっては仕様が満たされていませんね。

TDDサイクルの次なるフェーズはメソッドをリファクタリングしてメソッドがいつでも仕様を満たすようにすることです。
recoverHp() メソッドを次のように改良しましょう。

User.php
    public function recoverHp($user) {
        $user['User']['hp'] = $user['User']['mhp'];
        return $user;
    }

このようにすると、定数値ではなく、メソッド内で入力値を加工した値を返すことになり
抽象度が増します。その分、わかりにくさが増すのですが、テストを実行すると緑のバーは保たれています。

user_test_4.jpg

この例は単純すぎて実感しづらいかもしれませんが、これが TDD の最大の(?)恩恵である
コードの変更がテストによってサポートされている ということなのですね。

このように、リファクタリングフェーズはそれまでに作ったテストケースの成功を保ちつつ進めることになります。

DB アクセスとユニットテスト

TDD の流れそのものからはやや外れるかもしれませんが、テストケースと対象メソッドの作り方について一言。

今回テストを作成したメソッドは User モデルの配列を渡して、また User モデルの配列を返す、というものでした。
メソッド内ではDBアクセスを一切行っていません。
したがって自動的に作られたフィクスチャも使っていません。
対して、いままでの筆者の経験では、ユニットテストにあまり慣れていない人は次のような、
データの取得・加工・保存が一体となったメソッドを作成し、
フィクスチャを使ったテストを書く傾向があるようです。

User.php
// IDで指定したユーザーの hp を回復させる
public function recoverHpByUserId($userId) {
    $user = $this->findById($userId);
    $user['User']['hp'] = $user['User']['mhp'];
    return $this->save($user);
}

これをテストするにはたくさんのフィールドを書いたフィクスチャを用意し、
ユニットテストでも実際にテスト用のDBにアクセスしデータの読み出し、加工、書き込みを
行います。(後述するCakePHP公式ドキュメントのテストの章でもそのような例が挙げられています。)
このようなテストはフィクスチャの用意やDBに正しく書き込まれたことの確認など
大変煩雑なものになりがちです。
しかも、ユニットテスト内で DB アクセスを行うとテストの実行時間が長くなり、
何度もテストを行いながら開発を進める TDD ではかなりのストレスとなります。

しかし、ユニットテストの観点から見ると本来DBの読み出し (find) や書き込み (save) のメソッドは
それら自身のテストが別の場所でなされているべきであり、
ここでわざわざテストするものではないはずです。

ユニットテストでは今回作成したようにメソッドをデータの入出力とは分離させて、
加工部分のみをテストできるような設計にするのが良い手法といえるでしょう。

おわりに

これで CakePHP + PHPUnit での TDD のやり方を概観することができました。
PHP ユニットや、それを拡張した CakePHP のテスティングフレームワークでは
ユニットテストをうまく行うための様々な機能やテクニックが用意されています。
今回はほとんど触れられませんでしたが、興味のある方はぜひ勉強してみてはいかがでしょうか。

CakePHP 公式ドキュメントのテストの章
http://book.cakephp.org/2.0/ja/development/testing.html
や、PHPUnit の公式ドキュメント
http://phpunit.de/manual/3.7/ja/
には日本語でとても詳しい情報が充実しています。


さて、明日の TDD AdventCalendar は setoazusaさんです。よろしくお願いいたします。

d_nishiyama85
都内でプログラマーやってます。むかしは物理とか数学とか。Ph.D.
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした