TestCafe×BrowserStackで行うマルチブラウザ対応のE2Eテスト

[STORES.jp Advent Calendar 2019 - Adventar](https://adventar.org/calendars/4549) 19日目の記事です！
昨日は [@upinetree](https://twitter.com/upinetree) さんによる[コードの計測・可視化を、まずは手頃な道具（GAS＋データポータル）ではじめてみる](https://qiita.com/upinetree/items/c694ef9939a22c294628)でした！

今回はTestCafeとBrowserStackを使ったマルチブラウザ対応のE2Eテストについて簡単に紹介します。

フロントエンドでは辛みの一つとして複数ブラウザにおける動作確認があります。
Chrome、Safari、Firefox、Edge、IEと様々なブラウザが存在し、OSはPCでMacOSやWindows、SPではiOS、Androidと様々あり手動テストでの動作確認には何時間と潰してしまう事もよくあるかと思います。
上記構成でこれらを解決しようという内容です。

なおサンプルコードもGitHubに公開していますので手元で確認したい場合はcloneして触ってみてください。
（Nuxt.js環境で構築しています）

[GitHub - Kosuke0820/TestCafe-BrowserStack-E2E-Testing-on-Nuxt](https://github.com/Kosuke0820/TestCafe-BrowserStack-E2E-testing-on-Nuxt)

## TestCafeについて

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/2c3bf4a2-4c40-716b-bd04-c43231d1b961.png)

https://devexpress.github.io/testcafe/

TestCafeはOSSのE2Eテストフレームワークです。
E2Eテストフレームワークではその他に [Selenium](https://selenium.dev/) や [Nightwatch](https://nightwatchjs.org/) などがありますがTestCafeの特徴として

- WebDriverのインストールなどの初期設定が不要で、導入がとても簡単
- マルチブラウザテストが可能
- テストの開始前および各アクションの後に、ページの読み込みとXHRを自動的に待機してくれる為、wait処理周りでの意図しないエラーになりづらい
- `async/await`が使えるES2017やTypeScriptにも対応
- [Page Model](https://devexpress.github.io/testcafe/documentation/recipes/extract-reusable-test-code/use-page-model.html)というSelectorの情報を再利用できるテストパターンを持つ

などがあります。


## BrowserStackについて

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/86c6059f-dba0-71ca-320e-f1d679b87e3a.png)

https://www.browserstack.com/

BrowserStackは様々なデバイス、ブラウザでのテスト環境をWeb上で提供してくれるサービスです。

主な機能として

- [Live](https://www.browserstack.com/live) - 下記画像のように各デバイス、OS、ブラウザを起動できる仮想環境の提供。その組み合わせは2000以上！
- [Automate](https://www.browserstack.com/automate) - Liveで提供している全ての環境下で自動テストを実行できる。並列テスト実行も可能

があります。

今回はE2Eテストの実行環境としてAutomateを使っていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/4ff381f1-dc34-bae1-7183-ce1df5966720.png)


料金体系は以下の通りでAutomateが使用できるプランだと月129ドルで安くはありません。
なお新規の登録をするとLive30分、Automate90分のフリートライアルがついています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/422b149d-3914-f5e2-3f24-df9b0c2c2b31.png)




## 今回のページサンプル

それでは本題に入っていきます。
今回はログインページを想定した題材に進めていきたいと思います。
画面は2画面で大きく **ログインページ**と**マイページ**の2ページ構成です。
マイページはログイン状況に応じて表示が分岐されます。

|ログインページ|マイページ（ログイン前)|マイページ（ログイン後）|
|:---:|:---:|:---:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/0467ce2b-3ee5-9fac-10aa-3d43ef7f9300.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/4a6e1ea2-6d68-ac62-47e9-c4e8a4da5a85.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/d60f9569-e359-9276-2027-f00cffe0766a.png)|


## TestCafeのみでローカルテスト

まずはBrowserStackは使わずTestCafeでローカルテストをするところまで進めます。
TestCafeにはCLIでトリガーする方法と、Runnerクラスを使う方法がありますが、今回はCLIでの実行で進めていきます。

### 簡易サーバーの準備


今回は簡易サーバーとして [typicode/json-server](https://github.com/typicode/json-server) を使用していきます

```
yarn add -D json-server
```

そして仮想DBとして `db/db.json` を作成しユーザーデータを登録します。

```db/db.json
{
  "user": [
    {
      "name": "testuser",
      "password": "password"
    }
  ]
}
```

`package.json` のscriptsに以下を追記します

```
"server": "json-server --watch db/db.json --port 8000"
```

`yarn server` で立ち上げて準備完了です。

### ログインページ

dbに保存されているデータとinputに入力したユーザーネームとパスワードが合致すればログインという設定でロジックを組みます。
※本来こんなログインロジックはあり得ませんがサンプルの為、ご容赦ください🙏

他ページのコードは[リポジトリ](https://github.com/Kosuke0820/TestCafe-BrowserStack-E2E-testing-on-Nuxt)を参照してください

```pages/login.vue
<template>
  <div class="login-wrapper">
    <h1>Login</h1>
    <hr class="line" />
    <p v-if="isError" class="error-message">
      ユーザーネームもしくはパスワードが誤っています
    </p>
    <label>
      UserName:
      <input v-model="name" type="text" class="name" />
    </label>
    <label>
      Password:
      <input v-model="password" type="password" class="password" />
    </label>
    <button @click="login">Login</button>
  </div>
</template>

<script>
export default {
  data: () => ({
    name: '',
    password: '',
    isError: false
  }),
  methods: {
    async login() {
      const users = await this.$axios.$get('http://localhost:8000/user')
      const isCorrect = users.some(
        (user) => user.name === this.name && user.password === this.password
      )
      if (isCorrect) {
        this.$store.commit('SET_USER', this.name)
        this.$router.push('/')
      } else {
        this.name = ''
        this.password = ''
        this.isError = true
      }
    }
  }
}
</script>
```

### テストの作成

TestCafeをインストールします。


```
yarn add -D testcafe
```

続いて`package.json`のscriptsに追記します。

`chrome`と記載がある部分に `safari` や `firefox` などブラウザ名を指定します。
指定したブラウザはPC内にインストールされている必要があります。

[対応ブラウザ一覧 - Locally Installed Browsers](https://devexpress.github.io/testcafe/documentation/using-testcafe/common-concepts/browsers/browser-support.html#locally-installed-browsers)

`--speed`オプションはテストの実行スピードを指定できます。
今回は動きがわかるよう、少し遅めの`0.8`に設定しています。

オプションは他にも
テストが失敗するたびにスクリーンショットを撮影してくれる [takeonfails](https://devexpress.github.io/testcafe/documentation/using-testcafe/command-line-interface.html#takeonfails) や
テスト実行のビデオを記録してくれる [video](https://devexpress.github.io/testcafe/documentation/using-testcafe/command-line-interface.html#--video-basepath)
など便利なオプションが揃っています。

```
"test:e2e": "testcafe chrome test/e2e/*.js --speed 0.8"
```

ログイン部分のテストを書いていきます。

API部分は`RequestMock`でmock化しておきます。
`Selector`は `document.querySelector()` と同様にelementを指定することができます。
`inputの入力 → Mypageに遷移 → ユーザーネーム表示`の流れでテストしていますが、コードを見るだけでも直感的でわかりやすいかと思います。

```test/e2e/login.js
import { Selector, RequestMock } from 'testcafe'

const resData = [
  {
    name: 'test',
    password: '1234'
  }
]

const mock = RequestMock()
  .onRequestTo('http://localhost:8000/user')
  .respond(resData, 200, { 'Access-Control-Allow-Origin': '*' })

fixture('ログイン')
  .page('http://localhost:3000/login')
  .requestHooks(mock)

test('正しいログイン情報を入力後、mypageにuserNameが表示されること', async (t) => {
  const nameInput = await Selector('.name')
  const passwordInput = await Selector('.password')
  const loginButton = await Selector('button')

  await t
    .typeText(nameInput, 'test')
    .typeText(passwordInput, '1234')
    .click(loginButton)

  // --- Mypageへ遷移 ---

  const loginUserName = await Selector('.user-name').textContent

  await t.expect(loginUserName).eql('test')
})
```

### テストを実行

`yarn test:e2e` を実行するとブラウザが立ち上がり、自動テストが実行されていきます。


[![Image from Gyazo](https://i.gyazo.com/c6704ea82f2d7c9e14e91f6f33f58959.gif)](https://gyazo.com/c6704ea82f2d7c9e14e91f6f33f58959)


## BrowserStackを導入

ここまではいい感じですね。
しかしTestCafeだけでは、自PCにインストールされているブラウザでないと使えません。
MacではIEのインストールはできない為IEのテストができません。
そこで登場するのがBrowserStack Automateです。

### testcafe-browser-provider-browserstackのインストール

TestCafeが公式で提供している[testcafe-browser-provider-browserstack](https://github.com/DevExpress/testcafe-browser-provider-browserstack)をインストールします

```
yarn add -D testcafe-browser-provider-browserstack
```

### 環境変数の設定

環境変数に `BROWSERSTACK_USERNAME` と `BROWSERSTACK_ACCESS_KEY` を設定する必要があります。
https://www.browserstack.com/ から登録後、https://www.browserstack.com/accounts/settings のAutomateから上記2つの情報が取得できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/ee58c315-f0a9-90c7-1aa2-2843c5cc8b3d.png)

取得した値を`bash_profile` や `zshrc`など利用しているシェルに合わせて環境変数を設定します。

### テストの実行

scriptsを以下のように修正します。
先程とは違い、テスト実行するブラウザのバージョンやOSを設定しています。


```
"test:e2e": "testcafe browserstack:ie@11.0:Windows 10 test/e2e/*.js --speed 0.8"
```

`yarn test:e2e` のコマンドを叩き、https://automate.browserstack.com/dashboard/v2 を見てみるとテストが実行されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/229570/7deb8993-6a56-8ba8-735f-6eace421451d.png)

テスト終了後、ログから実行結果を動画で確認することが可能です。
Windows環境でIEのテストができました！🎉

[![Image from Gyazo](https://i.gyazo.com/21eb37fa391768655ff656b9046e2486.gif)](https://gyazo.com/21eb37fa391768655ff656b9046e2486)


## 所感

ローカルで試すだけであれば比較的容易に環境構築は可能。

E2Eのデメリットではありますが、効果的に書かないとテストの実行時間が膨大になってしまいます。
コアな部分でのピンポイントの利用に抑えて、できるだけ単体テストやスナップショットテストなどで担保するのが良さそうだなぁといった所感です。

手動テストとはお別れできないと思っていますが、このようなE2Eで手動テストのコスト、不安感は減る為、上手に活用していきたいですね！

明日20日目は [Charlie・Msfm](https://twitter.com/yakiimo23) さんによる「Podcastを運用するまで」です！