CakePHP + PHPUnitによる
実践的ユニットテスト
2013/07/06 ゆるかわPHP#2
若松 慶信(@yshnb)
自己紹介
• 若松慶信
• 1987/8/1生
• 所属:ヴォラーレ株式会社 Webエンジニア
CakPHP
CakePHP
• 学習コスト低
• 設置が簡単
• ActiveRecordが使い易い
• 情報が豊富
典型的なWebサービスの開発が容易
フレームワークの動向
Google Web検索の人気度
(日本、2012/07 ~ 2013/06)
CakePHP > その他
ちなみに世界では
Google Web検索の人気度
(すべての国、2012/07 ~ 2013/06)
CakePHPでのテスト
CakePHP1.x → SimpleTest
CakePHP2 → PHPUnit
PHPUnit
• xUnit系テスティングフレームワーク
– 最新はPHPUnit3.8
• コマンドラインから実行可能
– JenkinsなどCIツールと併用しやすい
ユニットテストの基本の話
テストの目的
• なぜテストするか?を考える
– バグを発見する
– 仕様通りの動作をするか検証する
テストの方針
• ユニットテストで守るべきルールは?
– コードカバレッジ100%?
– 同値分割・境界値分析で全パターンを網羅?
– あらゆる例外ケースの網羅?
テストの方針
• ユニットテストで守るべきルールは?
– コードカバレッジ100%?
– 同値分割・境界値分析で全パターンを網羅?
– あらゆる例外ケースの網羅?
(実践的)テストの方針
誰でも実践できる・変更に強い
「誰でも実践できる」とは?
• 高度なテスト・複雑なテスト
– 書いた人に依存しやすい
– プロダクトコードの変更時、
テストコードが無視されてしまう
できるだけ誰にでも分かる形で書く
変更への強さ
コードの変更 バグの発見
厳密なテスト 変更に弱い より多くのバグを発見できる
寛容なテスト 変更に強い 発見できるバグは限られる
テストの厳密性と発見できるバグの関係
具体例:アサートの使い分け
$actual = $this->Sample->getSomething();
$this->assertInternalType("array", $something);
$this->assertEquals($expected, $something);
寛容なアサート:戻り値の型チェックのみ
厳密なアサート:等価かどうかのチェック
実行
Q. 寛容なテストは意味があるか?
• 意外とこれで発見できるバグも多い
– もちろん寛容のレベルはケースバイケース
• テストは銀の弾丸ではない
– あくまでバグ発見手段の1つにすぎない
– 完璧主義に陥ってはいけない
CakePHP流テストの話
CakePHP流のテストの話
• MVC(Model2)フレームワーク
– 重いModel + 軽いController + α
– テスト対象の主役はModel
• オブジェクト間の依存
– 副作用をもつものもある
• 戻り値のチェックのみで十分なのは
副作用がない場合のみ
メソッドの副作用
public function url2link($url = null) {
return empty($url) ? $url : "<a href="{$url}">{$url}</a>";
}
副作用のないコードの例
副作用のあるコードの例
public function createHoge($data) {
$this->create();
return $this->save($data);
}
メソッドの実行は外部に影響しない
メソッドの実行が外部に影響する
副作用のテスト方法
• 2つのアプローチ
• 状態中心テスト
• 相互作用中心テスト
状態中心テスト
テストケース
テスト対象
オブジェク
ト
副作用実行
参照・検証
実行対象によって変化した状態を検証
相互作用中心テスト
テストケース
テスト対象
オブジェク
ト
(モック)
副作用実行
検証
モックの準備(エクスペクテーション設定)
実行対象が外部に与えようとする変化を検証
どちらのアプローチを使うか?
• Model (Component)
→ 状態中心テスト
• Controller (Behavior, Shell)
→ 相互作用中心テスト
1つの考え方
あくまで一例。例えば
Model間の依存が多い場合は、相互作用中心のテスト
というアプローチも全然あり
テストの流れ
• 事前準備
– テストに使うデータの生成など
• 実行
– テスト対象のメソッド実行
• 検証
– メソッド実行結果の検証(アサート)
• 後処理
– 使用したオブジェクトの破棄
Modelのテスト
• 事前準備
– データの準備はテストに含めておき
Fixtureには依存させない
• 検証
– 戻り値の検証
– 変更された状態の検証
– チェック目的が明確に分かるように
– アサートは必要に応じ別メソッドを用意
Modelのテスト例
// 事前準備(検証データの挿入)
$this->Hoge->save($this->sampleData);
// 実行
$data = $this->Hoge->getTableData();
// 検証
$this->assertIntenralType(‘array’, $data);
$this->assertDataFormat($data);
Controllerのテスト
• 厳密なユニットテストではない
• 相互作用中心のテスト
– Model, Componentなどをモックで扱い
相互作用を検証する
Controllerのテスト
• 事前準備
– Controller内のオブジェクトをモック化
• ControlllerTestCase::generate
– モックへエクスペクテーション設定
• 実行
– エクスペクテーションの検証
• 検証
– set内の値の検証など
Controllerのテスト例
// 事前準備
$this->Hoges = $this->generates(“Hoges”);
// 実行
$this->testAction(“/test/”);
// 検証
$this->assertArrayHasKey(“hoge”, $this->vars);
その他Tips
• dataProviderによる類似ケース管理
• プライベートメソッドのテスト
• モック使用時のアンチパターン
dataProviderでの類似ケース管理
• 渡すパラメータだけを変更して
実行過程を管理
• どんなケースに有効か?
– 同値分割・境界値分析で
パラメータ組み合わせを変えたケース
– バリデーションのテストケース
dataProviderでの類似ケース管理
public function dp_sample() {
$cases[] = array(1); // Case1
$cases[] = array(-1); // Case2
return $cases;
}
/**
* @test
* @dataProvider dp_sample
*/
public function dp_sample($arg) {
// …
}
プライベートメソッドのテスト
※ただし単体でテストせずに済む方法を探すほうが望ましい
• 別クラスのオブジェクトにして処理を委譲
• 呼び出し元の状態中心テストでカバー
• あきらめる
プライベートメソッドはテストケースから直接実行できない
→ どのように実行するか?
プライベートメソッドのテスト
// 事前準備
$__privateMethod = new ReflectionMethod(‘Subject’, ‘__privateMethod’);
$__privateMethod->setAccessible(true); // アクセス権限をpublicに変更
// 実行
$__privateMethod->invoke($this->Subject, null);
// 検証
// …
① ReflectionMethodを使ってアクセス権限書き換え
プライベートメソッドのテスト
Closure::bind(function() {
// 事前準備
$Subject = ClassRegistry::init(“Subject”);
// 実行
$Subject->doSomething();
// 検証
…
}, $this, “Subject”)->__invoke();
② Closure::bind() を利用して実行スコープを変更
モック使用時のアンチパターン
public function register() {
App::uses("File", "Utility");
$file = new File("/path/to/file");
if ($file->open()) {
// …
}
}
例えばプロダクトコードでの場当たり的オブジェクト生成
• あれ、テストのときもFile動いちゃうじゃんw
• モックへの差し替えが難しい
→ テストが難しくなる
→ テストを書かない・書いても複雑なテストに
public function startup() {
App::uses("File", "Utility");
$this->File = new File("/path/to/file");
}
public function register() {
if ($this->File->open()) {
$filetext = $this->File->read();
}
}
アンチパターンへの対処
• オブジェクトを委譲しておくことで対処
• しかし本質的に重要なのはテストの声を聞くこと
テストの書きづらさ=プロダクトコードの問題
ツールの簡単な紹介
Jenkins
Jenkins
Jenkinsでできること
• 出来ること一例
– テストの実行
– 各種レポーティング
– 静的解析ツールとの併用
• Mess Detector
• Code Sniffer
• Copy Paste Detecor
– ドキュメントの生成
Phing
こんなツールですよ
要はPHP版 Ant
※めんどくさくなってきたからこれだけ
ご清聴ありがとうございました

Cake php + php unitによる実践的ユニットテスト