【フロントエンド】テストが書かれていないプロダクトにE2Eテストを導入する

# :point_up: はじめに
本記事はテストを書かれていないプロダクトに比較的簡単にテストを導入するための記事になります。
また、本記事ではSPAでのテスト導入になります。

ソースコードはすべて[公開](https://github.com/Ut0n69/e2e-testing)しています。

# :keyboard: テストツール
- [ava](https://github.com/avajs/ava)
- [puppeteer](https://github.com/GoogleChrome/puppeteer)
- chromium

プラグインなど使用せずに標準で非同期通信に対応している```ava``` ```puppeteer``` を採用しました。
また、テストブラウザは```chromium```なのでIEなどでは別途検証が必要になります。

# :page_facing_up: テスト説明
## E2Eテストディレクトリ構造
```
test
└── e2e
   ├── config.js
   ├── index.js
   └── specs
      └── memberRegister.js
```

### config.js
```config.js``` はブラウザの設定などを記述します。

```js
module.exports = {
  URL: "http://localhost:8080",
  browser: {
    ignoreHTTPSErrors: true,
    headless: true,
    slowMo: 0,
    args: ["--no-sandbox", "--disable-setuid-sandbox"]
  },
  basicAuth: {
    userName: 'YOUR_USER_NAME',
    password: 'YOUR_PASSWORD',
  },
};
```

#### URL
テストを実行する環境を変更したい場合はURLを変更します。
URL: 'http\://localhost:8080'

#### ブラウザ設定
ベーシック認証の情報もconfig.jsにまとめてあげると良いでしょう。

ヘッドレスモードはGUIベースではなくてCLIベースでのテストになります。

``` headless: true``` の場合はバックグラウンドでCLIベースのテストが実行されます。
ブラウザが立ち上がらないため、テスト時間が短縮されます。

``` headless: false``` の場合は実際にブラウザが立ち上がり自動操作が行われます。

基本的には``` headless: true```  とし、ヘッドレスモードでテストを行います。
テストに失敗したり、実際に流れを確認したい場合にのみ```false``` に変更します。

### index.js
```index.js``` は実行するテストファイルを記述します。

```js
/*
  会員登録フローテスト
  ・入力
  ・入力不備によるエラーの出力チェック
  ・項目をすべて入力
  ・会員登録確定
*/
import memberRegister from "./specs/memberRegister";

(() => {
  memberRegister();
})();
```

#### コメントの記述
どの部分のテストか、どのような流れで操作を行うか
についてコメントを書くと良いでしょう。
**※ テストフローなどを修正した場合は必ずコメントも修正するよう心掛けます。** 

### specs
```specs``` ディレクトリにはテストしたいフロー毎にファイルを作成し格納します。

# :hammer_pick: ava puppeteerでのテスト記述方法
## モジュールなどの読み込み
```js
const puppeteer = require('puppeteer');
const test = require('ava');
const config = require('../config');
```
```ava``` ```puppeteer``` 及びconfig.jsを読み込みます。

## テストしたいフローを記述

- ブラウザのセットアップ
- エラー出力のチェック
- 項目をすべて入力
- 会員登録確定

```js
module.exports = function() {

  test.serial("Setup", async t => {
    // ここに操作を記述
  });

  test.serial("Show error", async t => {
    // ここに操作を記述
  });

  test.serial("Input", async t => {
    // ここに操作を記述
  });

  test("Completion", async t => {
    // ここに操作を記述
  });
};

```
入力 → エラー出力 → 入力 → 確定
順番通りに実行したい場合には上記のように```test.serial(){ }``` と記述します。
順番は関係なく並列実行したい場合は```test(){ }``` と記述します。

## 操作を記述
### 事前準備
操作するためにはE2Eテスト用に新しくクラスまたはidをそれぞれのタグに付与します。
今回はわかりやすくするためにidを付与していきます。

```html
<input type="text" />
```
↓

```html
<input id="e2e-input-name" type="text" />
```

### ページにアクセス
```js
await page.goto(`${config.URL}/login`, {
  waitUntil: 'networkidle2',
});
```
非同期通信のため、``` waitUntil: 'networkidle2',``` オプションを記述します。

### フォームに入力
```javascript
await page.type('セレクタを指定','入力したいテキスト');
```

例

```javascript
await page.type("#e2e-input-name", "Caroline");
```

### クリック
```javascript
await page.click('セレクタを指定');
```

### ラジオボタン
```javascript
await page.evaluate(() => {
  const element = document.querySelectorAll('セレクタを指定')[0];
  element.click();
});
```

ラジオボタンが複数ある場合は同じセレクタ名を付与し、配列のindexを指定して取得します。

### セレクトボックス
```js
await page.select('セレクタを指定', 'valueを指定');
```

## 注意点
SPAなのでDOMの描画はJSで処理されるので、ページ遷移には注意が必要です。

```js
await page.goto(`${config.URL}/login`, {
  waitUntil: 'networkidle2',
});

// 画面描画待ち
await page.waitForSelector('#e2e-input-name');
```
```page.goto``` メソッドを使用した直後は```page.waitForSelector``` メソッドを使用して
ページが描画されるまで待機する処理を記述します。

さらに、ローディング画面などを画面に重ねて表示している場合には```page.waitFor``` メソッドを使用して、
ローディング画面が外れるまで待機する処理も記述すると良いでしょう。

```javascript
await page.waitFor(1000);
```

# :pick: テスト実行方法

```package.json``` に下記スクリプトを追加

```
"test": "ava test/e2e/index.js --verbose"
```

下記コマンドを実行します。

```
npm run test
```

```  (テスト数) passed``` と表示されるとテスト成功になります。

# :earth_africa: ソースコードの公開
テスト対象サイト: https://member-register.herokuapp.com/
ソースコード: https://github.com/Ut0n69/e2e-testing

# :pray: おわりに
E2Eテストは比較的容易に導入できるため、テストが書かれていないプロダクトには導入をお勧めします。

デメリットとしてはメンテナンスが大変であること、またE2Eテストは壊れやすいことが考えられます。
さらに、導入した人がメンテナンスを続けなければいけない状況にもなりかねません。
しかし、テストが全くないことに比べると有益であるはずです。
修正による予期せぬ不具合にいち早く気付けることもあります。
E2Eテストの導入を検討してみてはいかがでしょうか。

以上