プロダクトコードの強制モック化 with PHP5.1

# モックを独自に作ろう
こんにちは(=ﾟωﾟ)ﾉ

今回は、PHPUnitを使ってUnitTestを書く時に大切になる**モック**を作る独自メソッドを作成しました。
理由としては、PHPUnitのバージョンを上げた時にメソッドの書き換えなどをしなくても済むようにするためです。

~~テスト書く前にバージョンアップしろよって話なんですがまぁそう簡単にはいかない大人の事情があるのです~~

# runkit関数を使う

今回は**[PHPのrunkit関数](http://php.net/manual/ja/ref.runkit.php)**を使用してメソッドをゴニョゴニョします。
この関数を使えば、**メソッドや組み込み関数の処理を動的に変更したり、有無を言わさず抹消**したりすることが出来ちゃう恐ろしくも中毒じみた威力を発揮する魔法です。
この関数を使って、**本来のメソッドの皮を被った偽物**を作り出しちゃおうというのが作戦です。

今回は例として、以下のようなメソッドに対してスタブを作成します。

```php
class TestClass {
    public function add10($a) {
        return $a + $this->return10();
    }
    
    private function return10(){
        return 10;
    }
}
```

今回目標とするのは

* ```return10()```メソッドのスタブを作り、```return10()```が10以外の値を返すこと
* ```add10()```を呼んだ時に本物の```return10()```が通らないこと
* 他のテストケースに影響しないこと

です。

今回はスタブを作り、テストコードをスムーズに書けることが目的なので、テスト駆動開発(TDD)を採用して進めようと思います。ですので、先に理想とするテストコードを書いておきます。

```php
class TestClassTest extends TestClass {
    public function test_generate_return10のスタブを作ること() {
        RunkitStub::generate('TestClass', 'return10', 50);
        $instance = new TestClass();
        $result = $instance->add10(10);
        $this->assertSame(60, $result);
    }

    public function tearDown() {
        RunkitStub::revertAll();
    }
}
```

TestClassTestとかいう色々ツッコミどころのあるクラス名になってしまいましたが許して下さい←
このテストを書くことによって、```generate()```の仕様をある程度固めながら実装を進めることが出来ます。
```generate()```の引数は、順に```クラス名, メソッド名, 期待する戻り値```です。

# runkit関数を使っていよいよスタブ化メソッドの実装

では実際に中身を実装していきます。
流れとしては以下の通りです。※以下、面倒なのでTestClassのコードを「本番コード」と呼ぶことにします。

1. 本番コードのクラス名とメソッド名を受け取り、ランダムな文字列にリネーム
2. メソッド名とランダムな文字列を紐づけるために、プロパティにスタック
3. 該当するメソッド名と同じ名前の偽メソッドを作成（この時、偽メソッドの中身は引数として受け取った値を返すだけ）
4. 偽メソッドが本番コードになりすましている間にテストを実行
5. テストが終わったら偽メソッドを消して、プロパティから本番コードを取り出し、元の名前にリネーム

少し分かりづらいかも知れませんが、要は**本番コードと同じ名前の偽物コードを生成してテスト実行時に代行してもらう**というかなり強引なやり方です。

では行きましょう(=ﾟωﾟ)ﾉ

#### 1. 本番コードのクラス名とメソッド名を受け取り、ランダムな文字列にリネーム
まず、受け取ったメソッドをリネームする必要があります。
この時に注意しなければならないのが、**リネームをする際は一意な値でなければならないこと**と、**リネームする前の本来のクラス名とメソッド名から切り離してはいけない**ことです。
メソッド名をランダムな文字列に変換する処理ですが、ここで```hash()```などを使いたいところですが、ハッシュ値にしてしまうと、例えば同じテストケース内で同じメソッド名を複数回スタブ化する場合に同一のメソッド名になってしまい、エラーとなってしまいます。
今回私は```uniqid()```というものを使い、'dummy'という文字列から一意な文字列を生成しました。

```php
$tmp_hashed_method_name = uniqid('dummy_');
runkit_method_rename($class_name, $method_name, $tmp_hashed_method_name);
```

#### 2. メソッド名とランダムな文字列を紐づけるために、プロパティに連想配列としてスタック
本番コードのメソッドに一時的に皮を被せたところで、ユニークな値とそのメソッドの情報をプロパティに格納します。
こうしないと、テストが終わった後に本番コードを元に戻せなくなります。~~ここが一番悩んだ~~ 

まずクラスの配列プロパティを宣言しておきます。

```php

private static $stashed_methods = array();
```

定義したプロパティに、ユニークな値とクラス名・メソッド名をぶっ込みます。

```php

self::$stashed_methods[] = array(
        'class_name'   => $class_name,
        'method_name'  => $method_name,
        'hashed_name'  => $hashed_name
);
```

これで本番コードを無事に退避させることが出来ました。

#### 3. 該当するメソッド名と同じ名前の偽メソッドを作成（この時、偽メソッドの中身は指定された戻り値を返すだけ）
さて、本番コードを避難させたところで偽物くんに登場していただきましょう。
ここでは、runkit_method_addを使用して引数として受け取ったクラス名とメソッド名、そしてreturnした値を元にスタブメソッドを作成します。

```php
runkit_method_add($class_name, $method_name, '', "return $return;");
```

runkit_method_addの引数とか詳しいことは公式ドキュメントをご参照ください。
この１行で、引数として受け取った値を返すだけのメソッドが作れちゃう！すごいや！！

#### 4. 偽メソッドが本番コードになりすましている間にテストを実行
頑張ってもらいましょう。

#### 5. テストが終わったら、偽メソッドには消えてもらう(この処理は、後のテストケースに影響が出ないようtearDownに記載)
さて、偽物を駆除する時間です。
テストが終わった後、作成したスタブを消さないと後のテストやカバレッジに大きく影響します。
ここで注意しなければならないことは、**後ろのスタブから順に削除すること**と、**プロパティの中身を全て削除する**ことです。

まず後ろのスタブから順に消す理由についてですが、１つのテストケースの中で複数回スタブを生成した場合に問題が発生しないようにするためです。
２回目以降に生成されたスタブは、**それより前の処理で作られたスタブから作られる**スタブだからです。
すんごい変な日本語で申し訳ないですが、１つのテストケースで複数回スタブを生成した場合、後ろから戻していかないと途中で辻褄が合わなくなってしまいます。それを防ぐために、プロパティにスタックされたメソッド達を一つずつポップします。

```php

while(!empty(self::$stashed_methods)){
    $origin = array_pop(self::$stashed_methods);
    runkit_method_remove($origin['class_name'], $origin['method_name']);
    runkit_method_rename($origin['class_name'], $origin['hashed_name'], $origin['method_name']);
}
```

こうすることで、次のテストケースに余計な情報を渡すことなく~~何事も無かったかのように~~テストを終えることが出来ます。

# カバレッジを見る
実際にRunkitStubクラスを使ってテストを実行して、カバレッジを見てみましょう。
先述したテストコードを実行して、カバレッジが以下のようになっていれば、```return10()```のスタブが上手く作られている証拠です。
<img width="384" alt="スクリーンショット 2019-01-15 18.28.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/264906/0788cbfa-ace2-0ea7-faab-da6b878b4fd5.png">

# まとめ
まとめるとこんな感じです。

```php
<?php
class RunkitStub {

private static $stashed_methods = array();

protected function generate($class_name, $method_name, $return) {
    $hashed_name = uniqid('Dummy__'); //同じメソッド名でも違う値になるようuniqidを使用
    runkit_method_rename($class_name, $method_name, $hashed_name);
        
    self::$stashed_methods[] = array(
        'class_name'   => $class_name,
        'method_name'  => $method_name,
        'hashed_name'  => $hashed_name
    );

    runkit_method_add($class_name, $method_name, '', "return $return;");
}

protected function revertAll() {
    while(!empty(self::$stashed_methods)){
        $origin = array_pop(self::$stashed_methods);
        runkit_method_remove($origin['class_name'], $origin['method_name']);
        runkit_method_rename($origin['class_name'], $origin['hashed_name'], 
$origin['method_name']);
    }
}
```

かなり強引なやり方ですので、勝手にやると怒られる場合があるので真似る際は自己責任でお願いします。

# 参考
[runkit関数]
http://php.net/manual/ja/ref.runkit.php