はらへり日記

腹に弾丸

Laravelでアプリケーションテストをする際のモックの話

アプリケーションテスト

要するに結合テストのことです。

Laravelでの単体テストは基本的にPHPUnitとLaravelによるヘルパーメソッドを使用することで楽に書けます。

アプリケーションテストもまた、ヘルパーが用意されており直感的に書くことができると個人的に思っています。

テスト 5.1 Laravel

しかしながらアプリケーションテストを書く際に少しばかり躓いたのでそれをサービスコンテナによって凌いだお話をします。

あるページであるデータが表示されてほしい

表題のようなテストを書くとします。

例えばBladeが下記の用な感じで

@foreach($user as $user)
  <div id="user">お名前:{{ $user->name }}</div>
@endforeach

コントローラーメソッドがこんな感じ。

public function userPage()
{
    $users = $this->userModel->getUsers();
    return view('index', ['user' => $users]);
}

このページにアクセスし、ユーザが表示されることを<div id="user">が表示されることで担保するのであれば以下のようなテストで確かめることができます。

public function accessToUserPage()
{
    $this->visit('/user')
        ->see('<div id="user">');
}

簡単ですね!しかしモデルメソッドの実装が複雑だとちと困ったことになります。

ここに複雑なモデルメソッドがあるじゃろ?

前述の$this->userModel->getUsers()の実装が例えばこうなっているとします。

public function getUsers()
{
    return ¥DB::table('users')
        ->leftJoin('hobby', 'user.hobby_id', '=', 'hobby.id')
        ->leftJoin('company', 'user.company_id', '=', 'company.id')
        ->where('user.deleted_flag', 0)
        ->where('hobby.id', '>', 100)
        ->where('compay.deleted_flag', 1)
        ->get();
}

2つのテーブルとINNER JOINし、それらのフラグも見た上でユーザをとっています。

この時、アプリケーションテストをする時の問題に以下のようなものがあります。

  • DBを叩く処理が走るのでテスト用DBを用意しなければならない
    • しかもテスト毎に同じ環境を作らなければいけない
  • テスト用データを作る際、getUsers()で1つ以上のデータが返ってくるようなデータを作らなければいけない

こんなん、規模がでかくなって走るモデルメソッドの数が増えたらあっという間につらくなります。

モックというソリューション

こうなると当然、UserModelgetUsers()をモック化する発想にいたります。

ここで僕は思います。単体テストUserModelに依存しているクラスをテストするのであればコンストラクターにモックを渡すことでメソッド内のUserModelの振る舞いを制御することが可能です。

しかしこれはアプリケーションテスト。この時のUserModelを一体どうやってモックに差し替えるんだ?リクエストに何かデータを持たせてそれをコントローラーから受け取れるようにしてそれでサービスクラスにそれを渡させてそれを見て逐一モックに切り替えるのそれともああああああああ

って僕はなりました。人類に結合テストは早かったのか?

そこで役に立つのがサービスコンテナです。

救世主サービスコンテナ

サービスコンテナとは

サービスコンテナについてはドキュメントを見てみてください。

サービスコンテナ 5.1 Laravel

他にもちょいちょい解説している記事を見かけるので、ggったりしてみてください。

ちなみにJavaDIコンテナと名前が違うだけで考え方は同じものです。

使い方

サービスコンテナを利用してこの問題を解決するには実装でサービスコンテナを利用している必要があります。

例えばサービスプロバイダーによってApp¥Models¥UserModelRepositoryに対して、UserModelクラスのインスタンスがサービスコンテナに登録されている状態だとしましょう。

プロバイダー内のregisterメソッドを抜粋すると以下の様な感じです。

public function register()
{
    $this->app->bind('App¥Models¥UserModelRepository', function ($app) {
         return new UserModel();
    });
}

これのサービスプロバイダーをconfig/app.phpに登録しておくことにより、アプリケーション内でuse App¥Models¥UserModelRepository;するとクロージャ内に書かれているようにUserModelインスタンスが注入されるようになります。

コントローラーで使用する方法は以下のようになります。

use App¥Models¥UserModelRepository as UserModel;

class MainController extends Controller {

    public function __constructor (UserModel $userModel) {
        $this->userModel = $userModel;  // ここでサービスコンテナに登録したインスタンスが返ってくる
    }
    
    /* 中略 */

    public function userPage()
    {
        // ここで使用されているUserModelはサービスコンテナから返ってきたインスタンス
        $users = $this->userModel->getUsers();
        return view('index', ['user' => $users]);
    }
}

テストでの解決策

ここまでサービスコンテナの説明みたいになってしまいましたが、本題であるUserModelピンポイントでモック化する方法をお話します。

結論としては以下のようなコードを書いてあげることでUserModelをモック化することができます!

public function accessToUserPage()
{
    $this->app->bind('App¥Models¥UserModelRepository', function () {
        return new UserModelMock();
    });
    $this->visit('/user')
        ->see('<div id="user">');
}

/** 中略 **/
class UserModelMock()
{
    public function getUsers()
    {
        /* ここでモックデータを作りreturnする */
        return $users;
    }
}

何をしているのかというと、以下の様な手順でモック化を実現しています。

  • テストメソッド内でサービスコンテナにApp¥Models¥UserModelRepositoryの名前で登録されている内容を上書きする
  • テストメソッドによりアプリケーションへのアクセスが発生する
  • コントローラーはApp¥Models¥UserModelRepositoryUserModelとして呼び出すのでテストクラス内で生成したモッククラスを返す
  • 実行されるメソッドも当然モッククラスのメソッド

というわけです。

これにより簡単にアプリケーションテストでクラスのモック化を実現することができました!!!

まとめ

普通にアプリケーションを書くだけだとクラス間の結合がある程度、疎になる以外のメリットがなかなか見いだせないサービスコンテナでしたが、今回の出来事で僕はすっかりサービスコンテナ推進派になりました。

かといって乱用はよくないのできちんとどんなメリットがあるかを理解した上で使ってみるとテスト、保守しやすいアプリケーションに少しずつ近づけるのではないかなと思います。

サービスコンテナ最高