レガシーコードとAspectMock

はじめまして、室内ならTシャツで問題ない技術推進室の篠崎です。
先日、piccaglianiさんのブログでAspectMockを知りました。
ちょうど私が対応しようとしていた課題にぴったしのフレームワークだったので少し書かせていただきます。
昔からあるコード
弊社では以下のようなコードがまだまだいらっしゃいます。(実際はこんな少ない行数ではありません)
がっつしArticle_Query
、Api
クラスに依存している状態です。
私が対応しようとしていた課題は「このような昔からあるテストのないコード、いわゆるレガシーコードに対してユニットテスト(PHPUnit)を作成していく」でした。
※そしてリファクタリングへ
ユニットテストで何を確認したいのか?
ユニットテストで確認したいのはSample_Service
クラスのgetAllArticles
メソッドの処理です。
Article_Query::findAll
メソッドの処理、DBにアクセスして意図どおりデータを返す(例えば0件なら空配列を返す)のかを確認したいわけではありません。
それはArticle_Query::findAll
のユニットテストで確認すべきものです。
Article_Query::findAll
とApi::getUser
はインターフェースに従ったデータを返してくれればいいだけで、実際にDBやAPIにアクセスしてデータを返す必要はありません。
0件の場合のテスト時にはこんな感じでデータを返してくれるだけでいいはずです、DBやAPIに対して何かしらの操作(テストデータを用意する、もとに戻すなど)は必要としないはずです。
class Article_Query { public static function findAll() { return []; } }
テスト用クラスの切り替え
テスト用に実装したクラスをどのように切り替えるのでしょうか?
昔は依存しているクラスをDI(Dependency Injection)のConstructor Injectionを利用して上記のようなテスト用クラス(テストダブル)に切り替えていました。
// ここでテスト用クラスの切り替え $articlQuery = new ArticleQuery_TestDobule(); $api = net Api_TestDobule(); $service = new Sample_Service($articlQuery, $api);
※ちなみに私がPHPで最初に見たDI ContainerはS2Container.PHPです(6年前ぐらい??)
テストダブルを作成するのが面倒だったのですが、現在ではテストダブルを作成する機能もPHPUnitなどのテスティングフレームワークが提供してくれています。
$articleQuery = $this->getMock('Article_Query'); $articleQuery->expects($this->any())->method('findAll')->will($this->returnValue([])); $service = new Sample_Service($articleQuery, $api);
残念ながら今回はテストダブルに切り替えたい対象がstaticメソッドなので単純にConstructor Injectionというわけにはいきません。
PHPUnitのテストダブル機能
ではどのようにユニットテストを作成すればいいのでしょうか?
staticメソッドであるArticle_Query::findAll
、Api::getUser
を切り替える事ができればいいのですが、PHPUnitのテストダブル機能ではできません。
あきらめてDBのテストデータやテストAPI(スタブ)を作る、もしくはrunkitの利用を検討していた所に、AspectMockが現れました。
AspectMock
PHPで実装されたモッキングフレームワークです。
PHPUnitのテストダブル機能であった制限がほぼありません。組み込み関数には適用できないぐらいです。
初期化処理、PHPUnitならbootstrapを(少し)記述する必要がありますが、テストコードは以下のような感じになります。
すごくないですか?ちょー簡単です。
以下の実装だけでstaticメソッドであるArticle_Query::findAll
は空配列を返すようになります。
test::double('Article_Query', ['findAll' => []]);
PHPUnitと同様にテストダブルは上記のようなスタブだけなくモックにもなります。
調べきれていませんが、PHPUnitの基本的なテストダブル機能は網羅されているのではないと思います。
何故できるのか?
何故staticメソッドにも適用できたのでしょうか?
AspectMockは内部的に対象クラスの全てのメソッドを書き換えます。
class Article_Query { public static function findAll() { if (($__am_res = __amock_before(get_called_class(), __CLASS__, __FUNCTION__, array(), true)) !== __AM_CONTINUE__) return $__am_res; // 実際のDB処理 } }
あわせて、requireされた時も書き換えたファイルが呼ばれるようになります。
require \Go\Instrument\Transformer\FilterInjectorTransformer::rewrite( $files , __DIR__)
__amock_before
によりメソッドが呼ばれた時にテストダブルを設定されているかを確認して、設定している場合はそちらを呼ぶという処理になります。
fakeMethodsAndRegisterCalls($class, $declaredClass, $method, $params, $static); } const __AM_CONTINUE__ = '__am_continue__';
なんとなく理解できましたでしょうか?
次回以降機会があれば、インストールや設定、既存オートローダーとの連携や、実装の詳細について紹介したいと思います。
最後に
件名にAspectMockをいれてるくせにすごく簡単な紹介になってしまいましたが、もし同様の事で困っていらっしゃる方いれば是非導入を検討してみてはいかがでしょうか?
あとこんなテストに困ってしまう設計は極力しないのが一番いいですよ!