Laravel自動テスト技法 ─ データベーステストとメソッドインジェクション

お疲れ様です。GMOインサイトの天河です。

天河は2024年1月をもって、GMOアドマーケティング株式会社からGMOインサイト株式会社に転籍となり、michill という女性向けウェブメディアの開発を担当していました。

働く女性の生活情報サイト|michill byGMO(ミチル)
michill by GMO

天河は転籍後の3ヶ月間のタスクの中で、michillの自動テスト/ユニットテストを実装してきました。200以上のメソッドのテストを実装し、プロダクト品質向上と、問題点の発見 / 改善に貢献することができました。

この記事では、Laravelのテストを実装する中で「このテスト、どうやって書いたらいいんだ?」と苦労したケースの解決方法を紹介したいと思います!

  • 「リレーションを持つデータベースに関するテスト」
  • 「データを偽装する必要があるテスト(依存性の注入 / DI)」

の実装方法がメインになります。上記二つはなかなか情報が無く苦労したので、同じ状況で困っている皆さんのお役に立てれば、と思います。
Laravel8を使用していますが、Laravel10, Laravel11 でも有用な内容なので、ぜひ見ていってください。
(なぜこの方法でできるか、の深掘りはそんなにしません。)

【使用言語】 Laravel 8.x, PHP 8.1(Laravel 10にしたい🥺)

想定ケース

簡単なブログサイトを想定して説明します。Articleテーブルをベースにテーブル間リレーションを確認しましょう。

Databases

  • Article:ブログ記事(基準)
  • Category:ブログカテゴリ(親)
  • User:ブログ執筆者(子)
  • ArticleKeyword:ブログキーワード(親)

Article.php

Category.php

ArticleKeyword.php

その他条件

  • hasOneの外部キーが、Category のIDになっています。
  • Seederは(個人的に)可読性が悪いという思想の元、使用していません。各テストケース内で必要となるデータは各テストケース内で作るほうが、可読性が高いと思っています。
  • 「技法」と銘打ってますが、そんな大層な方法は別にないです。

以上のケースにて直面するケースを説明します。

データベーステスト

hasManyを持つテストデータを作りたい

今回で言う、ArticleArticleKeyword 間リレーションですね。
以下のArticleFactory, ArticleKeywordFactory を用意します。

基本、belongsTo や morphTo 側(親テーブル)である、ArticleKeyword の外部キーに

'article_id' => Article::factory()

と、Factoryインスタンスを記述しましょう。子テーブル側は設定する必要はありません。
このhasMany関係を持つデータは、以下のようにして作ります。

こうすると、hasMany関係を持つデータを作成できます。ちなみに以下の件数データが作成されます。
hasManyは hasで定義しないと、belongsTo側のデータは作成されません。

外部キーが primaryキーであるhasOneを持つテストデータを作りたい

今回で言う、Article と Category のリレーションですね。
以下のArticleFactory, CategoryFactory を用意します。

Eloquentは、外部キーの値が親の主キーカラムに一致すると想定しています(ここで言うと category_id )。しかし今回は外部キーが id です。

先ほど「基本、belongsTo や morphTo 側(親テーブル)の外部キーにFactoryインスタンスを作成する」と書いていましたが、このケースでは、子テーブル側にFactoryインスタンスを作成しましょう。

この場合のhasOne関係は、以下のように作ります。色々作れるパターンがあります。

このケースは has を使わなくても、Factory の方で設定していれば、リレーションが設定されたデータが取得できます。しかし、has と <xxx>_id の片方だけ設定すると、 リレーションが設定されたデータが取得できません。

メソッドインジェクション

リクエストを偽装したい

メソッドインジェクションを利用します。テスト対象のメソッド内に Request::xxx() 系の処理があった時、以下のように対応します。

Request::root

Request::root()request()->root() の出力は一緒です。

Request::get

リクエスト偽装の方法として、Request::create を使用します。create するだけでなく、作成したrequestインスタンスをバインドし直して、インスタンスを偽装します。巷ではこれを「メソッドインジェクション」と言うみたいですね。

以下のような「クエリパラメータの値を参考に前後ページへのリンクタグを生成する」と言ったメソッドのテストに利用できます。

Request::server

こちらもメソッドインジェクションを利用しましょう。

Request::query

こちらもメソッドインジェクションを利用しましょう。

Request::ip

IPアドレスの偽装は、REMOTE_ADDR パラメータに値を入れます。

ページネーションインスタンスを偽装したい

LengthAwarePaginator を利用します。

Cookieの値を偽装する

今回紹介するユースケースとしては、

テストしたいCookie処理を含むメソッドがサービスファイルに存在しており、そのメソッド使用しているcontrollerのURLを呼ぶと色々副作用がありそうで面倒くさそうなので、そのメソッド単体を呼び出せるようにしてテストしたい

というケースです。

Cookie関連の処理を含むメソッドは、Cookieの特性上、URLリクエストを通してテストする必要があります。

この方法で取得されるCookieは暗号化されているので、暗号化処理のミドルウェアを無効にします。useWithoutMiddleware をインポートすると他のミドルウェアも無効になってしまうので、以下の記述を対象のテストケースの先頭に書きます。

テストしたいメソッドは以下です。Cookie::queue() メソッドが含まれている処理です。

次に、対象メソッドを呼び出すテスト用のパスを作っちゃいましょう。

Cookie::queue() は、リクエストの終了後にクッキーを設定する非同期関数なので、リクエストを走らせてレスポンスを取得する必要があります。そのために、テストリクエスト用のパスを作る必要があると言うことですね。そしてレスポンスにメソッドで取得したクッキーをセットする必要もあります。

こうして環境構築をして、出来上がったテストコードは以下のようになります。

 

ところで、公式のCookie偽装方法は、withCookie , withCookies を使用してGETリクエストを送信しています。 

しかしこのメソッドではミドルウェアを無効にしても暗号化がされたものが取得されます。なので以下をミドルウェアの無効化の記述の下に挿入します。

この場合の完成系は以下になります。

Cookieをランダムに返しているため、assertionは assertContains を利用していますが、取得できるCookieが自明の場合なら、assertCookieassertPlainCookie でアサーションしましょう。

こうしてCookieの処理を持つ関数を他に影響を及ぼすことなく / 考慮することなく、テストが出来ます。

おわりに

上で紹介したテスト方法はあまり情報がなく、実装に苦労したものをピックアップしました。
同じ状況に直面している方のお役に立てられたら幸いです。

役に立ったと思う方ははてブ、おかしい箇所があったり、そもそも内容が良くなかったと思う方は罵倒コメントください!

参考文献

Readouble  / Laravel 8.x データベーステスト

Readouble  / Laravel 8.x HTTPテスト

→ Laravel8ですが、最新のLaravelでも内容はあまり変わりません