Septeni Engineer's Blog

セプテーニエンジニアが綴る技術ブログ

「いまどき!?」CakePHPでPHPUnit入門

はじめまして、セプテーニ・テクノロジー(ベトナムハノイ)駐在の鎌田です。

昨今、社内グループにおける新規サービス開発の現場においても、RailsRspecScalaのScalaTest・Specs2等々、自然言語に近いテスト表記を用いた近代的なテスト駆動開発が主流になってきてます。

とは言え、数年前から運営中のサービス(=テストコードが無かったりする、いわゆるレガシーコード)だと、当然、既存のフレームワークの中で選択可能なテストツールの種類も限られてきます。
そんな中、今回はあえて(?)今のトレンドから若干離れてる気がしなくもないですが、CakePHP × PHPUnitな記事を書こうとおもいます。
想定の環境としては、あらかじめCakePHP 2.xが入ってる前提です。


1.PHPUnitのインストール

Pearからインストールする方法が一般的ですが、Pearから最新のPHPUnit(バージョン4.x)をダウンロードすると、CakePHPからPHPUnitを認識するために必要なファイルが解凍されない問題にハマったため、今回は別の方法でいきます。Composerというものを初めて使ってみました。

PHPUnitをインストールしたいフォルダへ移動

プロジェクトフォルダの内部にインストールします。すなわちComposerを使うと、プロジェクトごとに異なるバージョンのPHPUnitが容易に使い分けられて便利です。RailsのGemみたいですね。

cd app/Vendor


② composer.json(設定ファイル)というファイル名で以下記載

これもrailsで言うところのGemfileです。

vi composer.json
 {
  "name": "phpunit",
  "description": "PHPUnit",
  "require": {
   "phpunit/phpunit": "3.7.*"
  },
  "config": {
   "vendor-dir": "PHPUnit"
  }
 }

③ composerをダウンロード

wget http://getcomposer.org/composer.phar


PHPUnitライブラリを解凍

暗黙にcomposer.jsonを読み込んで、要求されたライブラリを解凍しているようです。この後、app/Vendor/以下に"PHPUnit"というフォルダが作成されているはずです。

php composer.phar install


CakePHP側でPHPUnitを読み込んでいるパスを書き換え

require_once APP . DS . 'Vendor' . DS . 'PHPUnit' . DS . 'autoload.php';


これでブラウザから "(ドメイン名)/test.php" にアクセスして、CakePHPユニットテストのページが表示されたらインストール完了。

2.基本的なテストケース

四則計算してくれるコンポーネント"CalculateComponent.php"に対して、テストケースを書きたいとします。

vi app/Test/Case/Controller/Component/CalculateComponentTest.php
App::uses('CalculateComponent', 'Controller/Component');
class CalculateComponentTest extends CakeTestCase {

    private $CalculateComponent = null;

    //SetUp
    public function setUp() {
        $this->CalculateComponent = new CalculateComponent;
        parent::setUp();
    }
    //TearDown
    public function tearDown() {
        unset($this->CalculateComponent);
        parent::tearDown();
    }

    // テストケース add(x, y)
    function testAdd() {
        $result = $this->CalculateComponent->add(1,2);
        $this->assertEquals(3, $result);
    }
    // テストケース multi(x, y)
    function testMulti() {
        $result = $this->CalculateComponent->multi(4,6);
        $this->assertEquals(24, $result);
    }


テストクラス名(ファイル名)は、「(テスト対象となるクラス名)+Test.php」となります。
また、テストメソッドは、メソッド名を「test+(テスト対象となるメソッド名など【先頭大文字】)」とするか、

    /*
     * @test
     */
    function add() {
        $result = $this->CalculateComponent->add(1,2);
        $this->assertEquals(3, $result);
    }

という風に@アノテーションをつけると、テストメソッドと見なされます。

setUp()、tearDown()は各テストメソッドの始めと終わりに一度ずつ呼ばれるメソッドです。
その他、各テストクラスに対してそれぞれ一度ずつ呼ばれるsetupBeforeClass()、tearDownAfterClass()もあります。

3.モック

PHPUnitに限らずテストコード開発においては、よくモック(スタブ)を用いることでテスト時のみの特別な挙動を操作したい場合があります。
ここに、実行時間帯によって実行結果が変わってくるsay()を含んだGreetingComponentクラスがあったとします。

vi app/Test/Case/Controller/Component/GreetingComponentTest.php
class GreetingComponent {

    private function getCurrentHour() {
        $hour = date("H");
        return $hour;
    }

    function say() {
        $greeting = "";
        $hour = $this->getCurrentHour();
        if(6 <= $hour && $hour <= 11) $greeting = "Good Morning.";
        else if(12 <= $hour && $hour <= 16) $greeting = "Good Afternoon.";
        else if(17 <= $hour && $hour <= 20) $greeting = "Good Evening.";
        else if(21 <= $hour && $hour <= 23) $greeting = "Good Night.";
        else if(0 <= $hour && $hour <= 5) $greeting = "ZZZ...";
        return $greeting;
    }

    // テストケース 朝
    function testSay_goodMorning_at7() {
        $mockGreetingComponent = $this->getMock("GreetingComponent",array("getCurrentHour"));
        $mockGreetingComponent->expects($this->any())
            ->method("getCurrentHour")
            ->will($this->returnValue(7));
        $result = $mockGreetingComponent->say();
        $this->assertEquals("Good Morning.", $result);
    }
    // テストケース 昼
    function testSay_goodAfternoon_at13() {
        $mockGreetingComponent = $this->getMock("GreetingComponent", array("getCurrentHour"));
        $mockGreetingComponent->expects($this->any())
            ->method("getCurrentHour")
            ->will($this->returnValue(13));
        $result = $mockGreetingComponent->say();
        $this->assertEquals("Good Afternoon.", $result);
    }
    ...(同じメソッドsay()に対して時間帯条件ごとに複数のテストケース)
}

  • $this->getMock()で、元のクラスを部分的にオーバライドしたモッククラス作成します。
    第一引数にターゲットとなるクラス名も文字列で、第二引数にオーバライドするメソッド名を配列で渡します。
  • expects()は当メソッドが何回実行されるかを期待します。any()は特に実行回数に制限が無い場合に指定します。any()の他にonce()など。
  • method()にはオーバライドしたいメソッド名を渡します。
  • will($this->returnValue($value))に返したい値"7"や"13"をセットします。

テスト実行時にこのメソッド(上記例では"getCurrentHour")を実行した時に、実行時間帯に関わらず

will($this->returnValue($value))

で指定した値が返ってきます。
同様にモックで"1"、"19"、"22"を返すようなテストケースも追加すると、「GoodEvening.」や「GoodNight.」など、全てのケースがテストできます。

FixtureとかJenkinsとの連携とかも書きたかったのですが、長くなりそうなので、また機会があれば書きたいと思います。
どうもありがとうございました。

参考文献
[CakePHP公式]
[cakephperの日記(CakePHP, Laravel, PHP)]

元記事はこちらから
[Qiita]