🤡

Laravel Mockery への勘違いと便利とおもったキッカケ

2022/03/15に公開

「モック」自体の浸透具合が浅すぎるからこそかもしれないので、ご容赦を。。

Mockery

mockery = 嘲笑、あざける

PHPのフレームワーク「Laravel」に標準パッケージとして入っている仕組みの一つ。言葉の意味としては、笑ってないけど笑ってる風に振る舞う、的な意味合いみたいで、日本語の「モック」と同じような言葉と思います。

https://readouble.com/mockery/1.0/ja/index.html

Mockeryはシンプルながら柔軟な、PHPモックオブジェクトフレームワークです。PHPUnitやPHPSpecなどのテストフレームワークと一緒にユニットテストで使用します。主なゴールは、可能性のある全てのオブジェクト操作を明確に定義できる簡潔なAPIと、人間が読み取れるDSL(Domain Specific Language)を使用した統合を提供することです。PHPUnitのphpunit-mock-objectsライブラリーの簡単に利用できる代替として設計しました。MockeryはPHPUnitと簡単に統合でき、難しさに絶望しなくともphpunit-mock-objectsと一緒に操作可能です。

公式はこんな説明をしているものです。なんなんでしょうね、この、詩的で、取り付く島もない的な感じ。文献もかゆいところになかなか手が届かない感じがしたので、個人的な解釈からの勘違いから、簡単に使い方までを残しておきます。


"モックデータ"を用意してくれるものではなく、クラスやメソッド自体を「モックする」もの

僕の一番の勘違いポイントです。

利用するきっかけはFeatureテストを作成するときで、データベースを介したくなかったという考えから。
Laravelのテストで、DBの検証までをカバレッジするときに Seeder + Factory でテストデータをDBに挿入してからCRUD系のテスト(厳密にはCreateはないですが)してみて、最後に RefreshDatabase とか DatabaseTransactions とかで元に戻す ... みたいな組み立て方になると思います。
この場合、DBにあらかじめセットしておくべきマスター系データがあったり、そもそもDBの操作をやりたくないセンシティブな環境だったりでは不向きに感じていて、DBを触らない方法があるかなーと思っていたところで出会った次第。

↑の経緯だったので、ちょろっと調べると「テストデータで検証ができる」みたいに聞こえてしまったものですから、勘違いしてしまったのでした。(そもそも業界経験が深くないというのが一番の要因と思います...)

実際の中身

Mockeryを使うと、使ったとき、かつ使うときに決めた範囲だけ、意図した返り値を返すメソッドに変更されたクラスファイル(ModelとかControllerとか)に差し代わった状態にすることができます。

例えばModelファイルをモックした結果をして出力されるインスタンスは、こんな感じ。

実行内容

$m = \Mockery::mock(\App\Horse::class)->makePartial();
dd($m);

結果

Mockery_1_App_Models_Horse^ {#2826
  #_mockery_expectations: []
  #_mockery_expectations_count: 0
  #_mockery_ignoreMissing: false
  #_mockery_ignoreMissingRecursive: false
  #_mockery_deferMissing: true
    
.....

                protected function incrementOrDecrementAttributeValue($column, $amount, $extra, $method){\n
            $argc = func_num_args();\n
            $argv = func_get_args();\n
            $ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);\n
            return $ret;\n
            }\n
            \n
                public function update(array $attributes = array (\n
            ), array $options = array (\n
            )){\n
            $argc = func_num_args();\n
            $argv = func_get_args();\n
            $ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);\n
            return $ret;\n
            }\n
            \n
                public function push(){\n
            $argc = func_num_args();\n
            $argv = func_get_args();\n
            $ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);\n
            return $ret;\n
            }\n
            \n
                public function save(array $options = array (\n
            )){\n
            $argc = func_num_args();\n
            $argv = func_get_args();\n
            $ret = $this->_mockery_handleMethodCall(__FUNCTION__, $argv);\n
            return $ret;\n
            }\n

.....

途中にメソッドが定義されているコードの中身も確認できるんですけど、成り代わりのクラスを丸々定義してくれて、模倣元のクラスの中身を出来るだけ模倣してくれています。

この状態で、実際にテストでメソッドをコールしたり、エンドポイントにPOST送信をかけるテストをしてみると、Mockeryが作ってくれたモデルインスタンス(Eloquentじゃないもの)に差し代わり処理が実行できるので、DBを介さず、テストが通せると。
ここまできて、当初先行してイメージしていた「意図したテストデータを返してくれることもできる準備が整う状態」になります。これが僕の勘違いで、一番苦戦したところ。。

あくまでも"返すころもできる"なので、insertやupdateをモックして、あたかも更新したことを再現する、といった使い方もできます。

実例

ではテストの実例なんですが、実際のコードを調整はしていますが、以下の感じで利用できます。

以下の例では、コントローラーで Horse モデルを使ったCRUDが構築されていて、horsesテーブルから get メソッドを使って、DBを介さずに意図した内容でレスポンスするロジックに変えてテストする、というケースを想定しています。

# モックインスタンスを作ります
$m = \Mockery::mock(\App\Horse::class)->makePartial();

# Factoryで作ったダミーデータを返すメソッドを定義します
# この定義の場合「\App\Horse::where(***)->get()」のメソッドの返り値を定義します
# whereの定義内容はなんでも良く、とにかく、whereとgetをHorseモデルから利用しているケースで適用されます
$data = factory(\App\Horse::class)->make();
$m->shouldReceive('where->get')->andReturn($data);

# モックしたクラスに、実際のクラスを差し替えます
$this->app->instance(\App\Horse::class, $m);

# あとはテストする
$response = $this->get(
    route('api.horse.show')
);
$response->assertOk();

DBがまだ出来上がってない、みたいなときでも使える

開発状況によりけりなのと、そもそも使わない、という手もある一方で、例えばチーム開発で追加のテーブル設計が必要になったときに、DBがないから確認できないねーってなるのではなく、Mockeryでモックしておいて、テストできるところは進められる、という考え方もできるなと思ってます。

こういう、進捗に依存する「ムラ」って実装作業中は結構なストレスだとおもうので、僕は積極的に"あざけて"いきたいなーと思っています。

Discussion