CodeceptionとAspectMockを使って和田卓人さんの現在時刻に依存するテストを書いてみた
TLで話題になっているCodeIQでの問題と解説はこちらです。
これであなたもテスト駆動開発マスター!?和田卓人さんがテスト駆動開発問題を解答コード使いながら解説します~現在時刻が関わるテストから、テスト容易性設計を学ぶ #tdd|CodeIQ MAGAZINE
大変丁寧な解説、とても参考になりました。
ただ、参加されている方の採用したテスティングフレームワークにCodeceptionがない!
日本のPHP開発者にCodeceptionを流行らせたいワタクシとしては看過できない事態(=チャンス)です!
ってことで、Codeceptionを使って書いてみました。
といいつつも、今回はCodeceptionがメインではなく、AspectMockがメイン。
AspectMockとは
Codeceptionの開発者の@davertさんによる「ちょーすげー(not an ordinary)」PHP向けのモッキングフレームワーク。
内部的にはGo! AOPが提供するアスペクト指向の概念を利用し、
- スタティックメソッドに対するテストダブルを作成できる
- あらゆるインスタンスメソッドに対するテストダブルを作成できる
- 処理中(on the fly)にメソッドを再定義できる
- 憶えやすいシンプルな構文
を実現しています。
AspectMockの裏側
詳しくは Understanding AspectMock に解説されていますので簡単に。
まず、GO! AOPの仕組みですが、独自のストリームラッパーを利用してincludeやrequireしたファイルをその場で解析、定義されているメソッドを抽出し、横断的関心事をウィービングするようです。
(これをPHPのみで実現しているのはすごいですね。処理重そうな感じしちゃいますが。。。)
AspectMockはこのGO! AOPの仕組みを利用し、全てのメソッドの一番最初に「Stubがあればそれ返してよ!」という処理をブッコミます。
<?php class User { function setName($name) { $this->name = $name; }
これが、、、
<?php class User { function setName($name) { if (($__am_res = __amock_before($this, __CLASS__, __FUNCTION__, array($name), false)) !== __AM_CONTINUE__) return $__am_res; $this->name = $name; }
こうなる。
アプローチ
AspectMockの仕組み上、PHPの標準関数に対するテストダブルは作成できないので、getHour
のようなメソッドを作成し、それに対するテストダブルを作成します。
言語 | テスティングフレームワーク | アプローチ | アプローチ詳細 |
---|---|---|---|
PHP5.4 | Codeception, AspectMock | 時刻ライブラリに介入とまではいかないけどむにゃむにゃ... | 標準関数のラッパーをAspectMockで stub |
といったところでしょうか.....
準備
1. Composer
以下の内容でcomposer.json
を用意してphp composer.phar install
します。
{ "require-dev": { "codeception/codeception": "1.8.*", "codeception/aspect-mock": "*" }, "autoload": { "psr-0": {"Greeter\\": "src/"} }, "config": { "bin-dir": "bin" } }
2. Codeception、AspectMockの初期化
bin/codecept bootstrap
します。
testsディレクトリ配下に必要なファイル群が作成されますので、tests/_bootstrap.php
を以下のように編集し、AspectMockを初期化します。
<?php // This is global bootstrap for autoloading $kernel = AspectMock\Kernel::getInstance(); $kernel->init();
3. クリーンアップ処理の定義
和田卓人さんも「組み込みクラス/メソッド/関数に介入する」アプローチの注意点として、以下のようにお書きでした。
このアプローチのデメリットは、強力さの裏返しです。力には責任が伴います。組み込みクラス/メソッド/関数の動きを変えるというのは、影響の大きい変更です。
(中略)
ゆえに、このアプローチを使う場合には、テスト後の始末を忘れずに行うことが重要です。テストの中で振る舞いを変えた標準クラス/メソッド/関数を元に戻しておかないと、他のテストにも影響が残ります。
tests/_helpers/CodeHelper.php
に後始末を以下のように記述します。
<?php namespace Codeception\Module; // here you can define custom functions for CodeGuy class CodeHelper extends \Codeception\Module { public function _after(\Codeception\TestCase $test) { \AspectMock\Test::clean(); } }
4. Unitテストの作成
bin/codecept genereate:test unit Greeter
を実行し、Greeterクラスに対する単体テストをtests/unit/GreeterTest.php
に作成します。
これで準備完了です。
Greeter
src/Greeter/Greeter.php
を以下のように実装しました。
<?php namespace Greeter; class Greeter { public function greet() { $hour = self::getHour(); if (5 <= $hour && $hour < 12) { return 'おはようございます'; } if (12 <= $hour && $hour < 18) { return 'こんにちは'; } return 'こんばんは'; } private static function getHour() { return date("G", time()); } }
getHour
は標準関数time
を使い、時間に変換して返します。
ここをAspectMockでstubすれば、テスタブルになります。
GreeterTest
tests/unit/GreeterTest.php
に以下のようにテストを記述しました。
<?php use AspectMock\Test as test; class GreeterTest extends \Codeception\TestCase\Test { protected $codeGuy; /** * @dataProvider greetProvider */ public function testGreet($hour, $message) { test::double('\Greeter\Greeter', ['getHour' => $hour]); $greeter = new \Greeter\Greeter(); $this->assertEquals($message, $greeter->greet()); } public function greetProvider() { return array( array(4, 'こんばんは'), array(5, 'おはようございます'), array(6, 'おはようございます'), array(11, 'おはようございます'), array(12, 'こんにちは'), array(13, 'こんにちは'), array(17, 'こんにちは'), array(18, 'こんばんは'), array(19, 'こんばんは'), ); } }
[ポイント1] CodeceptionのUnitテストではPHPUnitのメソッドを利用できる!
\Codeception\TestCase\Test
はPHPUnit_Framework_TestCase
を継承しているため、
$this->assertEquals($greet, $greeter->greet());
等、普通に使えます。
もう、さっさとPHPUnitからCodeceptionに乗り換えましょう。
[ポイント2] パラメタライズドテスト(Parameterized Test)
和田卓人さんも言及していた、PHPUnitのデータプロバイダ、Codeceptionのバージョン1.8から利用できるようになりました!
だから、さっさとPHPUnitからCodeceptionに乗り換えましょう。
[ポイント3] テストダブル
今回の本命AspectMockを使い、
test::double('\Greeter\Greeter', ['getHour' => $hour]);
で、GreeterクラスのgetHourメソッドを stub してます。
実際のテスト結果
bin/codecept run unit
します。
Codeception PHP Testing Framework v1.8.0 Powered by PHPUnit 3.7.28 by Sebastian Bergmann. Unit Tests (1) ------------------------------------------------------ Trying to test greet with data set #0 (GreeterTest::testGreet) Ok Trying to test greet with data set #1 (GreeterTest::testGreet) Ok Trying to test greet with data set #2 (GreeterTest::testGreet) Ok Trying to test greet with data set #3 (GreeterTest::testGreet) Ok Trying to test greet with data set #4 (GreeterTest::testGreet) Ok Trying to test greet with data set #5 (GreeterTest::testGreet) Ok Trying to test greet with data set #6 (GreeterTest::testGreet) Ok Trying to test greet with data set #7 (GreeterTest::testGreet) Ok Trying to test greet with data set #8 (GreeterTest::testGreet) Ok --------------------------------------------------------------------- Time: 11.3 seconds, Memory: 44.75Mb OK (9 tests, 9 assertions)
パスしました。