CodeceptJSのAutoLoginでログインが必要なテストを爆速にする

## はじめに

E2Eテストの開発を低速化させる要素の一つに **ログイン** があります。
機能そのもののテストに入る前に必ずログイン処理が入るようだと、開発がなかなか進まないですね。
一度ログインしたら、次回以降はログインを省略できるようにできないものでしょうか？

CodeceptJSには [AutoLogin](https://codecept.io/plugins#autologin) というプラグインがあり、これを有効にするとファイル（またはメモリ）にログイン直後のCookieを書き出して、次回実行時に再利用してくれます。
なにやらいかにも便利そうな感じですね。実際に使ってみましょう。

## 事前準備

CodeceptJSとSelenium(WebdriverIO)を使います。
事前にNodeJS V8.9以上をインストールしておいてください。

以下の説明はUNIX系OS(MacOS, Ubuntuなど)向けに書かれており、Ubuntu18.04で動作確認しています。
Windowsの方は適宜読み替えてください。

```bash
## WebDriverとかいろいろダウンロードされてヘヴィーです。
## Wifiあるところでやらないとギガがすごい減ります。

$ mkdir sample && cd sample # テスト用フォルダを作る
$ npm init # npmプロジェクトを作成

# 色々聞かれるがEnterを連打してお茶を濁す

$ npm install codeceptjs webdriverio selenium-standalone # ライブラリをインストール
$ npx selenium-standalone install # WebDriverのインストール
$ npx codeceptjs init

# 色々聞かれるがEnterを連打し（略

$ ls
codecept.conf.js  output/            package.json
node_modules/     package-lock.json  steps_file.js

# Selenium Serverを起動しておく
# サーバ起動中はこのターミナルは使えないので他のターミナルを使う
# 停止したいときは Ctrl + C 
$ npx selenium-standalone start
```

本筋とは関係ありませんが、 [retryFailedStep](https://codecept.io/plugins#retryfailedstep) を有効にしておくとテストコードの記述が楽になるので、有効にしておきます。
生成された `codecept.conf.js` を以下のように編集してください。

```codecept.conf.js

exports.config = {
  tests: './*_test.js',
  output: './output',
  helpers: {
    WebDriver: {
      url: 'http://localhost',
      browser: 'chrome'
    }
  },
  include: {
    I: './steps_file.js'
  },
  // --- ここから追記 ---
  plugins: {
    retryFailedStep: {
      enabled: true,
    },
  },
  // --- ここまで追記 ---
  bootstrap: null,
  mocha: {},
  name: 'codecept-sample'
}
```

## テストコードの記述

それではテストコードを書いていきましょう。
ログインが絡むテストということで、Qiitaを題材にしたテストシナリオを2つ用意してみました。

- ログインすると投稿ボタンが表示される
- ログインすると自分の記事に編集ボタンが表示される

ちなみにこのテストですが、 **自分で書いた記事が一つもない場合失敗します** 。
テスト設計が甘い？いやいや、小さな記事でもかまわないので、是非あなたの知見もアウトプットしてみてください！

まずはautoLoginを使わず素朴に書いてみましょう。
メールアドレスとパスワードはサンプルですので、適宜ご自身の認証情報に置き換えてください。

```qiita_test.js

Feature('Qiita')

Scenario('ログインすると投稿ボタンが表示される', (I) =>{
  I.amOnPage('https://qiita.com/login')
  I.fillField('ユーザー名 または メールアドレス', 'foo@example.com')
  I.fillField('パスワード', 'yourpassword')
  I.click('Qiita にログイン')
  I.see('投稿する')
})

Scenario('ログインすると自分の記事に編集ボタンが表示される', (I) => {
  I.amOnPage('https://qiita.com/login')
  I.fillField('ユーザー名 または メールアドレス', 'foo@example.com')
  I.fillField('パスワード', 'yourpassword')
  I.click('Qiita にログイン')
  I.click('div.st-Header_loginUser')  // 右上のユーザーアイコン
  I.click('マイページ')
  I.click('article')  // 記事をクリック
  I.see('編集する')
})
```

テストコードは先ほど作成したフォルダに `qiita_test.js` という名前で保存します。
テストの実行は以下のようなコマンドで行います。

```bash

$ npx codeceptjs run qiita_test.js
```

さて、二つのシナリオでそれぞれログイン処理を書いているので、冗長ですし、実行時間も長くなってしまいますね。
ここで**autoLogin**の出番です。
先程の `codecept.conf.js` を以下のように編集します。

```codecept.conf.js
exports.config = {
  tests: './*_test.js',
  output: './output',
  helpers: {
    WebDriver: {
      url: 'http://localhost',
      browser: 'chrome'
    }
  },
  include: {
    I: './steps_file.js'
  },
  plugins: {
    retryFailedStep: {
      enabled: true,
    },
// ===== ここから追記 =====
    autoLogin: {
      enabled: true,
      saveToFile: true, // trueだとCookieをファイルに書き出す、falseだとメモリに保持しテスト終了時に破棄する
      // ログインユーザーの定義
      users: {
        user: {
          // ファイルまたはメモリにcookieが保存されている場合、restoreを実行しcookieを復元する
          restore: (I, cookie) => {
            I.amOnPage('https://qiita.com/')
            I.setCookie(cookie)
          },
          // ログイン状態が保持されているかどうかをcheckで検証する
          // 今回の場合は、ログイン時に右上に表示されるアイコンが存在するかどうかを確認している
          check: (I) => {
            I.amOnPage('https://qiita.com/')
            I.waitForElement('div.st-Header_loginUser')
          },
          // checkでログイン状態でないと判定された場合は再度ログインする
          // ログイン後、cookieを取得しファイルまたはメモリに保存する
          login: (I) => {
            I.amOnPage('https://qiita.com/login')
            I.fillField('ユーザー名 または メールアドレス', 'foo@example.com')
            I.fillField('パスワード', 'yourpassword')
            I.click('Qiita にログイン')
            I.waitForElement('div.st-Header_loginUser')
          },
        },
      },
    },
// ===== ここまで追記 =====
  },
  bootstrap: null,
  mocha: {},
  name: 'codecept-sample'
}
```

これで**autoLogin**が有効になったので、今度はテストコードの方をautologinを利用するように修正します。

```qiita_test.js

Feature('Qiita')

Scenario('ログインすると投稿ボタンが表示される', (I, login) =>{
  login('user')
  I.see('投稿する')
})

Scenario('ログインすると自分の記事に編集ボタンが表示される', (I, login) => {
  login('user')
  I.click('div.st-Header_loginUser')  // 右上のユーザーアイコン
  I.click('マイページ')
  I.click('article')  // 記事をクリック
  I.see('編集する')
})
```

早速実行してみます。

```bash

$ npx codeceptjs run qiita_test.js
```

一つ目のシナリオではログイン処理が走りますが、二つ目のシナリオではログイン処理が省略されるのが分かるかと思います。
また、一度実行すると、二度目からはどちらのシナリオでもログインが省略されます。

保存されたCookieは `./output/user_session.json` に格納されています。

## ハマりどころ

### ログインがちゃんと終わるのを待つ

当初、 `restore` は走っているのにログイン状態が復元されずハマっていたのですが、 `login` 実行時、ログインボタンを押してからトップページに遷移するより前にCookieを取得してしまい、ログインが完了した状態のCookieを保存できていなかったのが原因でした。
ログイン後、右上にアイコンが表示されるのを明示的に待つようにするとうまくいきました。

```javascript

login: (I) => {
  I.amOnPage('https://qiita.com/login')
  I.fillField('ユーザー名 または メールアドレス', 'foo@example.com')
  I.fillField('パスワード', 'yourpassword')
  I.click('Qiita にログイン')
  I.waitForElement('div.st-Header_loginUser') // ログインが完全に終わるのを待つ
},
```

### restoreは対象サイトに移動してから

考えてみれば当たり前なのですが、Cookieをセットできるのは現在開いているサイトに対してのみなので、テスト対象のサイトに移動してから `I.setCookie(cookie)` しないとうまく動きませんでした。

```javascript

restore: (I, cookie) => {
  I.amOnPage('https://qiita.com/') // テスト対象のサイトに移動してからsetCookie()しないとだめ
  I.setCookie(cookie)
},
```

## おわりに

いかがでしたでしょうか？
同様の機構は[TestCafe](https://github.com/DevExpress/testcafe)などにも備わっていますが、ファイルに保存して次回実行時にも使いまわせるのは自分の知る限りだとCodeceptJSだけです（やってることは単純なので、他のフレームワークでも簡単に実装できそうではありますが……）。
ログインが絡むWebアプリを開発していて、テストコード記述の効率を上げたい人は是非参考にしてみてください。

それでは、良いテストライフを！