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

ログイン、投稿、編集、削除のブラウザテストを記述し、実行します。
![001.png](https://qiita-image-store.s3.amazonaws.com/0/27816/167ac2b2-a843-09bc-2fde-19cabe6b65a6.png)


# 親記事
[Laravel 5.7で基本的なCRUDを作る - Qiita](https://qiita.com/sutara79/items/ef30fcdfb7afcb2188ea)


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

:link: readouble.com: [各テスト後のデータベースリセット](https://readouble.com/laravel/5.7/ja/database-testing.html#resetting-the-database-after-each-test)

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

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

- データベース名: laravel57<strong>-dusk</strong>
- 照合順序(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実行時に自動で選ばれます。
:link: readouble.com: [環境の処理](https://readouble.com/laravel/5.7/ja/dusk.html#environment-handling)

ファイルの内容は下記のようにします。
:page_facing_up: [.env.dusk.local](https://github.com/sutara79/demo-laravel-crud/blob/5.7/.env.dusk.local)

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

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

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

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

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

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

以下のブラウザテストでは、[ファクトリ](https://readouble.com/laravel/5.7/ja/database-testing.html#writing-factories)を使ってテスト用にDBのレコードを生成しています。
そのファクトリの内部では[Faker](https://github.com/fzaninotto/Faker)を利用しているので、あらかじめインストールしておいてください。
Fakerがないとテスト実行時に下記のようなエラーが表示されます。
`Error: Class 'Faker\Factory' not found`

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

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

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

![001.png](https://qiita-image-store.s3.amazonaws.com/0/27816/2dcadfb6-0392-c228-b3e6-3955ffd0febd.png)

![002.png](https://qiita-image-store.s3.amazonaws.com/0/27816/13794ff4-7b31-c0ae-5206-a1bc922a50b2.png)

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

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

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

# ログインのテスト
:link: readouble.com: [テストの生成](https://readouble.com/laravel/5.7/ja/dusk.html#generating-tests)

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

ファイルの中身を下記のようにします。
:page_facing_up: [tests/Browser/LoginTest.php](https://github.com/sutara79/demo-laravel-crud/blob/5.7/tests/Browser/LoginTest.php)

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

## テストでは英語に固定する
上のテストコードでは、ログインボタンをクリックする際は`press('Login')`のようにボタンのテキストを指定しています。
テキストよりもname属性を指定するのが無難ですが、Auth関連で自動生成されたビューでは`<button type="submit" class="btn btn-primary">`のようにname属性が記載されていません。
ヘッドレスブラウザではロケールは英語となるようですが、[前回の記事](https://qiita.com/sutara79/items/95a5c59830006c715ae5#%E3%83%86%E3%82%B9%E3%83%88%E3%81%AE%E5%8B%95%E4%BD%9C%E3%82%92%E7%9B%AE%E3%81%A7%E7%A2%BA%E8%AA%8D%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88)のように`'--headless'`を無効にして実際にブラウザを起動させると日本語となる場合があり、そうなるとボタンのテキストは`ログイン`となります。
これではDuskがボタンを探し出せずにエラーとなってしまいます。
なお、[Laracastsでの質問](https://laracasts.com/discuss/channels/testing/get-translated-text-in-waitfortext-dusk-dusk-dusk)によると、テストコード内では`__()`などのLaravelの関数は使えないようです。

`press`メソッドの引数には、ボタンのテキスト、value属性、id属性、name属性の他に、CSSセレクタも受け付けています。
:link: DuskでHTML要素の探索を担当しているソースコード: [ElementResolver](https://github.com/laravel/dusk/blob/4.0/src/ElementResolver.php#L41)

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

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

```php: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つのモデルを」という方針を徹底すべきようなので、それに従うことにします。

[以前の記事](https://qiita.com/sutara79/items/c913d7eedd5456635ea5)でメール確認を有効にしているので、下記のように`email_verified_at`カラムに日付を挿入しなければ、たとえテストであっても記事を投稿することができません。

```diff: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`のようにしてモデル名を指定すると後が楽です。
:link: readouble.com: [ファクトリの生成](https://readouble.com/laravel/5.7/ja/database-testing.html#generating-factories)

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

ファイルの中身は下記のようにして、題名と本文をランダムに作成します。
:page_facing_up: [database/factories/PostFactory.php](https://github.com/sutara79/demo-laravel-crud/blob/5.7/database/factories/PostFactory.php)

## テストを記述する
```text:PowerShell
# テストファイルを生成
> php artisan dusk:make PostTest
```

ファイルの中身を下記のようにします。
:page_facing_up: [tests/Browser/PostTest.php](https://github.com/sutara79/demo-laravel-crud/blob/5.7/tests/Browser/PostTest.php)

ファクトリを利用する時は`create`と`make`の違いに注意してください。
`create`はデータを自動生成して、さらにデータベースに保存します。
`make`は自動生成するだけです。
:link: [モデルの保存](https://readouble.com/laravel/5.7/ja/database-testing.html#persisting-models)
:link: [モデルの生成](https://readouble.com/laravel/5.7/ja/database-testing.html#creating-models)

記事の削除では、削除確認のモーダルが表示されるまで待つために`whenAvailable`メソッドを使います。
:link: [利用可能時限定のセレクタ](https://readouble.com/laravel/5.7/ja/dusk.html#waiting-for-elements)

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


# ポリシーのテスト
```text:PowerShell
# テストファイルを生成
> php artisan dusk:make PolicyTest
```

ファイルの中身を下記のようにします。
:page_facing_up: [tests/Browser/PolicyTest.php](https://github.com/sutara79/demo-laravel-crud/blob/5.7/tests/Browser/PolicyTest.php)

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

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

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

Laravel本家には[assertStatus](https://readouble.com/laravel/5.7/ja/http-tests.html#available-assertions)というアサーション・メソッドがありますが、Duskでは使えません。
本家でもDuskでも内部でPHPUnitのアサーション・メソッドを利用していますが、両者は全く独立していて、Duskが本家のメソッドを内包しているわけではありません。
Duskの`$browser`のどこかで[Illuminate\Http\Responseのインスタンス](https://readouble.com/laravel/5.7/ja/responses.html#creating-responses)を取り出せないかと探してみましたが、ダメでした。
これではHTTPステータスコードを取得できません。
よって、上の方法しかありませんでした。
:link: [本家のアサーション・メソッドが定義されているファイル](https://github.com/laravel/framework/blob/5.7/src/Illuminate/Foundation/Testing/TestResponse.php)
:link: [Duskのアサーション・メソッドが定義されているファイル](https://github.com/laravel/dusk/blob/4.0/src/Concerns/MakesAssertions.php)


# 注意: ブラウザテストのカバレッジは取得できない
```text:PowerShell
> php artisan dusk --coverage-html cover-dusk
```

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