Laravel5.7: ブラウザテストを記述する


ログイン、投稿、編集、削除のブラウザテストを記述し、実行します。

親記事

Laravel 5.7で基本的なCRUDを作る - Qiita

Dusk用のDBと環境ファイルを用意する

前のテストがその後のテストデータに影響しないように、各テストの後にデータベースをリセットできると便利です。インメモリデータベースを使っていても、トラディショナルなデータベースを使用していても、RefreshDatabaseトレイトにより、マイグレーションに最適なアプローチが取れます。テストクラスてこのトレイトを使うだけで、全てが処理されます。

readouble.com: 各テスト後のデータベースリセット

リセットしても普段のローカル環境に影響が及ばないように、Dusk用のDBと環境ファイルを用意します。
Laravel 5.4で導入されたLaravel Duskをテスト後にDBリセットさせるようにして試してみた - Qiita

Dusk用のDBもローカルと同じくMySQLを使うことにします。

  • データベース名: laravel57-dusk
  • 照合順序(Collation): utf8mb4_unicode_ci
PowerShell
# ユーザー名「root」、パスワードなしで接続
> mysql --user=root --password=

# 長いので改行しています。
MariaDB [(none)]> CREATE DATABASE `laravel57-dusk`
       CHARACTER SET utf8mb4
       COLLATE utf8mb4_unicode_ci;

Dusk用の環境ファイルを作成します。
.env.dusk.localと名付ければ、ローカルでのDusk実行時に自動で選ばれます。
readouble.com: 環境の処理

ファイルの内容は下記のようにします。
.env.dusk.local

【重要】 .envのコピー

公式ドキュメントにあるとおり、ブラウザテストを実行すると.envファイルの内容が一時的に書き換わります。
具体的には下記の流れをたどるようです。

  1. .envのコピーが作られ、.env.backupと命名される。
  2. .envの内容が.env.duck.localの内容に置き換わる。
  3. ブラウザテスト終了。
  4. .envの内容が.env.backupに置き換わり、元に戻る。
  5. .env.backupが削除される。

しかし、テスト中にエラーが有ると、.envの内容が.env.dusk.localの内容のままで元に戻らないことがあります。
一方、.env.backupは削除されます。
そうなると、もともとの.envの内容は完全に消えてしまいます。
.envはGitで管理されないので、復元不可能です。

そのため、私はあらかじめ.envをコピーして.env.mycopyと命名して保存しています。
当然、.env.mycopyもGitで管理すべきではないので、設定を追加します。

.gitignore
# Duskのテストでエラーがあると .env が壊れる場合があるため
# 設定ファイルを手動でコピーして保存しておく
.env.mycopy

Laravel5.4ではFakerが必要

(Laravel5.5以降は最初からFakerが依存パッケージに指定されているので、下記の操作は不要です)

以下のブラウザテストでは、ファクトリを使ってテスト用にDBのレコードを生成しています。
そのファクトリの内部ではFakerを利用しているので、あらかじめインストールしておいてください。
Fakerがないとテスト実行時に下記のようなエラーが表示されます。
Error: Class 'Faker\Factory' not found

PowerShell
# Fakerをインストール
> composer require-dev fzaninotto/faker

# 私は、本番環境のHerokuでも使いたいので「require」でインストールしました。
> composer require fzaninotto/faker

ブラウザのサイズを指定する

以下のテストでは、グローバルナビ内の「投稿する」ボタンをクリックするという動作を指示しています。
しかし、ブラウザの幅が狭いとレスポンシブ・デザインによってグローバルナビ内のメニューが隠れてしまいます。

こうなると「ボタンが存在しない!」とテストに判断されてエラーとなってしまいます。
そこで、テスト用のブラウザのサイズを指定して、PC向けの広い画面でテストすることを明示します。
Bootstrap4のブレークポイントの基準で「Large devices(992px以上)」となるように横幅を指定します。

tests/DuskTestCase.php
         $options = (new ChromeOptions)->addArguments([
             '--disable-gpu',
-            '--headless'
+            '--headless',

+            // Bootstrap4のブレークポイントの基準で「Large devices(992px以上)」となるよう横幅を指定。
+            // ただし、ウィンドウ枠などを考慮して余裕をもたせる。
+            '--window-size=1100,600',
         ]);

ログインのテスト

readouble.com: テストの生成

PowerShell
# テストファイルを生成
> php artisan dusk:make LoginTest

ファイルの中身を下記のようにします。
tests/Browser/LoginTest.php

PowerShell
# テストを実行する
> php artisan dusk --filter 'LoginTest'

テストでは英語に固定する

上のテストコードでは、ログインボタンをクリックする際はpress('Login')のようにボタンのテキストを指定しています。
テキストよりもname属性を指定するのが無難ですが、Auth関連で自動生成されたビューでは<button type="submit" class="btn btn-primary">のようにname属性が記載されていません。
ヘッドレスブラウザではロケールは英語となるようですが、前回の記事のように'--headless'を無効にして実際にブラウザを起動させると日本語となる場合があり、そうなるとボタンのテキストはログインとなります。
これではDuskがボタンを探し出せずにエラーとなってしまいます。
なお、Laracastsでの質問によると、テストコード内では__()などのLaravelの関数は使えないようです。

pressメソッドの引数には、ボタンのテキスト、value属性、id属性、name属性の他に、CSSセレクタも受け付けています。
DuskでHTML要素の探索を担当しているソースコード: ElementResolver

よって、press('[type="submit"]')とすれば日本語にも英語にも対応できます。
今回はこれで解決できますが、特定のメッセージが表示されたかどうかをテストしたい場合もあるでしょう。
そのメッセージがロケールによって異なる場合、テストコードを書く手立てがありません。
そこで、環境がtestingの場合は英語に固定するように、ミドルウェアのhandleメソッドの冒頭に追記します。
Stack Overflow: php - Forcing locale in Laravel Dusk

これで、多言語対応している場合でもページ内の文章に依存したテストを書けるようになります。

app/Http/Middleware/CheckLocale.php
    public function handle($request, Closure $next)
    {
        // テストでは使用言語を英語で固定する
        // Dusk内では __() などは使えないため
        // 参照: https://laracasts.com/discuss/channels/testing/get-translated-text-in-waitfortext-dusk-dusk-dusk
        if (\App::environment('testing')) {
            \App::setLocale('en');
            return $next($request);
        }

CRUDのテスト

User用のファクトリを修正する

Userモデルのファクトリはデフォルトで用意されています。
なお、はじめから用意されているファクトリのファイル名は、Laravel5.4ではModelFactory.phpでしたが、5.5以降はUserFactory.phpです。
「1つのファイルには1つのモデルを」という方針を徹底すべきようなので、それに従うことにします。

以前の記事でメール確認を有効にしているので、下記のようにemail_verified_atカラムに日付を挿入しなければ、たとえテストであっても記事を投稿することができません。

database/factories/UserFactory.php
     return [
         'name' => $faker->name,
         'email' => $faker->unique()->safeEmail,
         'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
+        'email_verified_at' => $faker->dateTime(),
         'remember_token' => str_random(10),
     ];

Post用のファクトリを作る

Postモデルのファクトリは存在しないので、新たに生成します。
ファクトリを作る際、--model=Postのようにしてモデル名を指定すると後が楽です。
readouble.com: ファクトリの生成

PowerShell
> php artisan make:factory PostFactory --model=Post

ファイルの中身は下記のようにして、題名と本文をランダムに作成します。
database/factories/PostFactory.php

テストを記述する

PowerShell
# テストファイルを生成
> php artisan dusk:make PostTest

ファイルの中身を下記のようにします。
tests/Browser/PostTest.php

ファクトリを利用する時はcreatemakeの違いに注意してください。
createはデータを自動生成して、さらにデータベースに保存します。
makeは自動生成するだけです。
モデルの保存
モデルの生成

記事の削除では、削除確認のモーダルが表示されるまで待つためにwhenAvailableメソッドを使います。
利用可能時限定のセレクタ

PowerShell
# テストを実行する
> php artisan dusk --filter 'PostTest'

ポリシーのテスト

PowerShell
# テストファイルを生成
> php artisan dusk:make PolicyTest

ファイルの中身を下記のようにします。
tests/Browser/PolicyTest.php

PowerShell
# テストを実行する
> php artisan dusk --filter 'PolicyTest'

オーナーではない記事を普通のユーザーが編集しようとすると403エラーになることを確認します。
具体的には、403エラー用のビューに設置しておいたHTML要素<span class="error-code">403</span>を手がかりにして、下記のように確認しています。

$browser->assertSeeIn('.error-code', '403');

Laravel本家にはassertStatusというアサーション・メソッドがありますが、Duskでは使えません。
本家でもDuskでも内部でPHPUnitのアサーション・メソッドを利用していますが、両者は全く独立していて、Duskが本家のメソッドを内包しているわけではありません。
Duskの$browserのどこかでIlluminate\Http\Responseのインスタンスを取り出せないかと探してみましたが、ダメでした。
これではHTTPステータスコードを取得できません。
よって、上の方法しかありませんでした。
本家のアサーション・メソッドが定義されているファイル
Duskのアサーション・メソッドが定義されているファイル

注意: ブラウザテストのカバレッジは取得できない

PowerShell
> php artisan dusk --coverage-html cover-dusk

上のコマンドでcover-dusk/フォルダ内にカバレッジの結果が一応生成されます。
しかし、結果は正常ではありません。
実行されたはずのコントローラのアクションのカバレッジが0%となっています。
PHPUnitの@coversアノテーションで明示してもダメです。
まあ、アクションのページにブラウザがアクセスしているというだけで、テストコード内ではアクションのメソッドを全く実行していないのだから仕方ありません…。
@coversNothingという、カバレッジの対象外とするためのアノテーションが存在し、その説明の中で「This can be used for integration testing. 」とあるくらいですから、結合テストやブラウザテストではカバレッジを取得できないのが当たり前なのでしょう。