OctoberCMSでLaravel Dusk + Seleniumを使ってE2Eテストをする

Seleniumのアドベントカレンダーからこの記事を見て「OctoberCMSって何」と言う人のために少し説明すると、OctoberCMSとはLaravelベースのCMSです。まだまだ流行ってませんが、いくつかあるLaravelベースのCMSの中ではダントツの完成度です。WordPressよりも技術的な知識を必要としますが、Laravelベースだったり、拡張性・柔軟性が高いので、とても開発がしやすいです。

2017/12現在、OctoberCMSのドキュメントにはE2Eテストを実施する方法が[Functional testの説明](https://octobercms.com/docs/help/unit-testing#testing-system)としてあるが、自分がトライしたところChromeに接続できないなど問題があったり、使っているパッケージのバージョンが古くてメンテされていないようだ。

逆に、Laravel5では[Dusk](https://laravel.com/docs/5.5/dusk)というテストフレームワークが含まれているので、せっかくならばそれを利用したい。OctoberCMSのドキュメントでは触れられていないが、OctoberCMSでDuskを使うことができたので、手順をまとめてみた。

ちなみに、Duskがどんなものかというと、結局のところ[PHPUnit](https://phpunit.de/)と[facebook/php-webdriver](https://github.com/facebook/php-webdriver)と[Selenium](http://www.seleniumhq.org/)（またはスタンドアロン[chromedriver](https://sites.google.com/a/chromium.org/chromedriver/)）をLaravelベースのプロジェクトで使うためのパッケージだ。

## 更新
DockerコンテナからホストマシンのSeleniumにアクセスするのにホストのIPを指定する方法を記載していたが、現在では `docker.for.mac.localhost` でアクセスできるので、IP周りの込み入った話を削除した。

## 前提
* 完全に自分の都合だが、OctoberCMSプロジェクトはDockerコンテナ内で走らせている
* OctoberCMSはLaravel5.5ベースのバージョン（Dusk使うにはLaravel5.5ベースが必要）
* コンテナ内の実行環境はPHP7+nginx（Laravel5.5ベースのOctoberCMSはPHP7が必須）

Laravel5.5ベースのOctoberCMSを使う方法は「[OctoberCMSの導入](https://qiita.com/pikanji/items/7017e4264d960db27210)」で説明している。`edgeUpdates`のあたりが参考になる。



## インストール
### Selenium と Chrome Driver のインストール
自分の場合、

* 他のブラウザも将来的に対応したいのでSelenium + chromedriverで運用する
* ブラウザが入っていないDockerコンテナでOctoberCMSを走らせているため、ブラウザの入っているローカルマシンにこれらをインストールする

ローカルマシンでOctoberCMSを走らせている場合、Chromeだけを使うならばこれらをインストールする必要はない。Duskはスタンドアロンchromedriverをデフォルトで使用するようになっている。

下記を**ローカルマシン(コンテナのホストマシン)**で実行してSeleniumをインストール。

```shll
brew update
brew install selenium-server-standalone
selenium-server --version
```

こちらもローカルマシンで実行してchromedriverをインストール。

```shell
brew install chromedriver
```

### Dusk のインストール
#### composerに追加
Duskは[Laravelのドキュメント](https://laravel.com/docs/5.5/dusk)に書いてある様にcomposerでインストールする。こちらはコンテナ内で実行。注意：Octoberが使用しているLaravelのバージョンは5.5(2019/07/09現在)で、これに合うduskバージョンは2.xになる。バージョン3以降だとサポートから外されてしまっている。

```shell
composer require --dev laravel/dusk:^2.0
```
自分のコンテナ内ではrootでログインするのでcomposerコマンドを実行した後はプロジェクトのファイルの権限を修正してやる必要がある。例えば自分の場合はdocker-syncでファイルをローカルとコンテナで同期させているので、docker-syncの`sync_userid`のユーザに`chown`してやる必要がある。例えば、自分の環境の場合 `chown -R www-data:www-data *`。


### Service Providerの追加
開発環境のみで `DuskServiceProvider` が有効になるようにする。

* プロジェクトルートに `config/dev` ディレクトリを作成
* `config/app.php` を作成した `config/dev/` にコピー
* .envファイルに `APP_ENV=dev` を追記
* `config/dev/app.php` の `providers` に `Laravel\Dusk\DuskServiceProvider` を追加

```php
'providers' => array_merge(include(base_path('modules/system/providers.php')), [
    ...
    'Laravel\Dusk\DuskServiceProvider',
]),
```

### インストールコマンド実行
コンテナ内のプロジェクトルートで下記を実行。念のため上記同様の理由で`chown`も実行。

```shell
php artisan dusk:install
chown -R www-data:www-data
```

## 設定
### クラスが認識されるようにする
もし `tests/CreatesApplication.php` がプロジェクトになければ下記の内容で作成。内容は[ここ](https://github.com/laravel/laravel/blob/develop/tests/CreatesApplication.php)から拝借。

```php
<?php namespace Tests;

use Illuminate\Contracts\Console\Kernel;

trait CreatesApplication
{
    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__.'/../bootstrap/app.php';
        $app->make(Kernel::class)->bootstrap();
        return $app;
    }
}
``` 

作成したPHPファイルと `tests/DuskTestCase.php` を composer.json に追記して認識されるようにする。

```json
"autoload-dev": {
    "classmap": [
        "tests/CreatesApplication.php",
        "tests/DuskTestCase.php",
        ...
    ]
},
```

代わりに下記でも良さそう。

```json
"autoload-dev": {
    "psr-4": {
        "Tests\\": "tests/"
        ...
   }
}
```

そして、下記を実行。再度、念のため`chown`も。

```shell
composer dump-autoload
chown -R www-data:www-data
```

### DuskTestCaseを修正
もし、テストをローカルマシン、またはブラウザが入っているコンテナ内で実行するのであれば、このステップは不要。

デフォルトの`DuskTestCase`の実装では、テスト実行場所のchromedriverを自動で起動してそこに接続するようになっているので、自分の様に、テストをブラウザが入っていないコンテナ内で実行する場合は、ブラウザがあるローカルマシンのselenium（またはchromedriver）に接続する必要がある。

tests/DuskTestCase.php を以下のように修正する。

* `static::startChromeDriver();` をコメントアウト
* `RemoteWebDriver::create` 呼び出し時のURLパラメータを `http://docker.for.mac.localhost:4444/wd/hub` に変更。

```php
public static function prepare()
{
    //static::startChromeDriver();
}

/**
 * Create the RemoteWebDriver instance.
 *
 * @return \Facebook\WebDriver\Remote\RemoteWebDriver
 */
protected function driver()
{
    $options = (new ChromeOptions)->addArguments([
        '--disable-gpu',
        '--headless'
    ]);

    return RemoteWebDriver::create(
        'http://docker.for.mac.localhost:4444/wd/hub', DesiredCapabilities::chrome()->setCapability(
            ChromeOptions::CAPABILITY, $options
        )
    );
}
```

#### タイムアウトを伸ばす
* もし、テスト実行してタイムアウトエラーになる場合、下記のように`RemoteWebDriver::create`に引数を渡してタイムアウト時間を変更できる。
* 自分の場合、１分ではタイムアウトになるので、３分(180*1000)に変更して解決した。

```php
return RemoteWebDriver::create(
    'http://docker.for.mac.localhost:4444/wd/hub', DesiredCapabilities::chrome()->setCapability(
        ChromeOptions::CAPABILITY, $options
    ), 180*1000, 180*1000
);
```

もしそれでも解決しない場合、こちらが参考になるかも。
https://laracasts.com/discuss/channels/testing/dusk-doesnt-work-app-updated-to-laravel-54/replies/311354


## テストの実行
Duskにはサンプルテスト (`tests/Browser/ExampleTest.php`) がついてくるので、これを実行してセットアップできているか確認できる。

### サンプルテストの修正
サンプルテストはまっさらなLaravelプロジェクトでパスするテストなので、ここではOctoberCMSのdemoテーマでパスするように修正する。
`ExampleTest.php` はロードしたページに"Laravel"の文字列があるかどうかをチェックしているが、これを"October CMS"に変更する。

```php
public function testBasicExample()
{
    $this->browse(function (Browser $browser) {
        $browser->visit('/')
                ->assertSee('October CMS');
    });
}
```

### Seleniumの起動
ローカルマシンで下記を実行して、Seleniumを起動する。chromedriverは自動的にロードされる。

```shell
selenium-server
```

### テスト実行
プロジェクトが走るコンテナ内のプロジェクトルートで下記を実行することでテストを走らせることができる。

```shell
php artisan dusk
```

テストの実行は2分近くかかる。

テストに失敗した場合など、自動的にスクリーンショットを撮って、デフォルトでは `tests/Browser/screenshots` に格納してくれる。


## おまけ
[Seleniumをローカルマシンではなく別のコンテナで走らせる](https://medium.com/@adiach3nko/how-to-run-laravel-dusk-inside-docker-container-21c630d48aa4)というのも１つの選択肢だ。ローカルマシンにSeleniumを入れなくて良かったり、ホストのIPの設定をしなくて良くなるので、少しだけきれいになるのではないか。