はじめまして、セプテーニ・テクノロジー(ベトナム・ハノイ)駐在の鎌田です。
昨今、社内グループにおける新規サービス開発の現場においても、RailsのRspec、Scalaの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クラスがあったとします。
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;
}
}
このメソッドに対してそのままテストコードを書いて実行したとすると、テスト実行時間帯が朝か夜かで、アサーション結果が変わってしまいそうです。
時間に依存するメソッドに対するテストは、モックを使うと良いです。以下、PHPUnitでのモックの書き方です。
App::uses('GreetingComponent', 'Controller/Component');
class GreetingComponentTest extends CakeTestCase {
// テストケース 朝
function testSay_goodMoring_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との連携とかも書きたかったのですが、長くなりそうなので、また機会があれば書きたいと思います。
どうもありがとうございました。