Nightwatchを使ったe2eテストでcsvから値をセットするカスタムコマンドを作る

# 結論
csvで作ったデシジョンテーブルっぽいcsvから値を取得し、指定された項目に設定するカスタムコマンドを作成する。

[プロジェクトはこちら](https://github.com/frost-star/vuetest)

[できあがったものがこちらになります](https://github.com/frost-star/vuetest/blob/1.0.0/test/e2e/custom-commands/setCsvValue.js)

# やりたいこと
こんな感じのフォームをつくったとします。
今回はVue.jsとElementで作りました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115996/c8c324e0-8a6d-17d1-762e-b0f85ed96e35.png)

入力項目が5つあり、Sign Upを押したときに入力項目にエラーがなければ登録されます(今回は登録の処理は抜きにして、代わりに一番下のラベルに結果が出るようになっています)。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115996/d38cedeb-e1a5-d812-7cbe-c10bffaa809c.png)

これに対するテストを考えたとき、デシジョンテーブルを書いてみると大体こんな感じ。
〇がついている項目がそのケースで採用される値で、暗い部分がエラーケースです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/115996/7cc0ab9c-6705-bfbd-11b0-a215049bb5b4.png)

当たり前ですが、これを手作業でテストすると面倒。
Vue.jsでは[Nightwatch](http://nightwatchjs.org/)でe2eテストが自動でできる環境が整っているので、それを使ってみると下記のようになります。
`browser.currentTest.name`は現在のテストケース名(下記の場合case1)を取得しています。

```js
  'case1': function test(browser) {
    const devServer = browser.globals.devServerURL;
    browser
      .url(devServer)
      .waitForElementVisible('#app', 5000)
      .setValue('#name input', 'abcdefghijkl')
      .setValue('#age input', '100')
      .setValue('#password input', 'abcdefgh')
      .setValue('#passconf input', 'abcdefgh')
      .setValue('#desc textarea', 'abcdefghij')
      .click('#btn-signup')
      .assert.containsText('#result span', 'success')
      .saveScreenshot('./ss/' + browser.currentTest.name + '.png')
      .end();
  },
```

毎回`setValue`を書かなきゃないのが結構面倒なので、下記のようにcsvのパスとケース名を指定したらその部分の値を自動で各項目に設定してくれる`setCsvValue`というコマンドを作ります。

``` js
  'case1': function test(browser) {
    const devServer = browser.globals.devServerURL;
    browser
      .url(devServer)
      .waitForElementVisible('#app', 5000)
      .setCsvValue('test/e2e/decision_table.csv', browser.currentTest.name)
      .click('#btn-signup')
      .assert.containsText('#result span', 'success')
      .saveScreenshot('./ss/' + browser.currentTest.name + '.png')
      .end();
  },
```

# setCsvValueコマンドを作る
## csv読み込みを準備する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/115996/c525a6bc-87ac-7d78-c859-0b75ba36fbde.png)

読み込む用のCSVを作成します。
といっても、selectorの行を追加して、そこに各要素のセレクタを記述したくらいです。
`#name input`のようになっているのはElementを使っているためinput要素に直接idを付与できなかったためです。

また、`npm install csv --save-dev`コマンドを使って、[csv](https://www.npmjs.com/package/csv)を入れておいてください。

## カスタムコマンドを作る
`test/e2e`の下に`custom_commands`フォルダを作り、そこにカスタムコマンドを作っていきます。
カスタムコマンドでは、ファイル名がそのままコマンド名になるので`setCsvValue.js`ファイルを作成し、処理を記述していきます。

```setCsvValue.js
const fs = require('fs');
const csvPerser = require('csv-parse/lib/sync');

exports.command = function(filePath, caseName, callback) {
  // 列名の設定
  const selectorColumn = 'selector';
  const valueColumn = 'value';
  const enableMark = '〇';

  // csv読み込み
  let csv;
  try {
    let stream = fs.readFileSync(filePath);
    csv = csvPerser(stream, {columns: true});
  } catch (err) {
    console.log(err);
    throw 'Unable to open file: ' + filePath;
  }

  // テストデータの整形
  let testData = {};
  csv.forEach((row, index, csv) => {
    if (row[selectorColumn] !== '') {
      this.selector = row[selectorColumn];
    }
    if (row[caseName] === enableMark) {
      testData[this.selector] = row[valueColumn];
    }
  }, {selector: ''});

  // 値の設定
  Object.keys(testData).forEach((key) => {
    this.setValue(key, testData[key]);
  });
}
```

処理は大きく4つに分かれます。

### 列名の設定
CSVで必要となる列名を定義します。
selectorColumnはセレクタ列、valueColumnは入力値の列を指定します。
また、enableMarkにはどの値を使うか表している文字(今回の場合は〇)を指定します。

### CSVの読み込み
CSVファイルからデータを読み込みます。
csvParseのオプションで`{columns:true}`を使うことで1行目を列名として扱い、オブジェクトの配列としてCSVを取得することができます。
例えば、2行目のデータは以下のようなオブジェクトになります。

```js
  { item: 'Name',
    selector: '#name input',
    description: '12文字',
    value: 'abcdefghijkl',
    case1: '〇',
    case2: '',
    case3: '',
    case4: '',
    case5: '',
    case6: '〇',
    case7: '〇' },
```

### テストデータの整形
読み込んだCSVからテストデータを扱いやすい形に整形します。
各行の情報を見ていき、引数で渡された`caseName`のプロパティ値が`enableMark`と一致する`selector`と`value`を格納していきます。
この時、`selector`が空の場合に一番近い上の行にある値を取得するために、一時的なオブジェクトに値を保持しながら繰り返します。
最終的に以下のようなオブジェクトが得られます。

```js
{ '#name input': 'abcdefghijkl',
  '#age input': '100',
  '#password input': 'abcdefgh',
  '#passconf input': 'abcdefgh',
  '#desc textarea': 'abcdefghij' }
```

### 値の設定
`setValue`を使って各項目に値を設定します。

## カスタムコマンドを使う
`nightwatch.conf.js`に`custom_commands_path`を指定します。

```nightwatch.conf.js
  custom_commands_path: ['test/e2e/custom-commands'],
  custom_assertions_path: ['test/e2e/custom-assertions'],
```

後はテストケースとして追加していくだけ。

``` js
  'case1': function test(browser) {
    const devServer = browser.globals.devServerURL;
    browser
      .url(devServer)
      .waitForElementVisible('#app', 5000)
      .setCsvValue('test/e2e/decision_table.csv', browser.currentTest.name)
      .click('#btn-signup')
      .assert.containsText('#result span', 'success')
      .saveScreenshot('./ss/' + browser.currentTest.name + '.png')
      .end();
  },
```

このテストケースは、csvのケース名とテストケース名を合わせているので、setCsvValueの第二引数を`browser.currentTest.name`にしています。
そのため、テストケース名だけを変えるだけで各テストケースを実行することができます。
`saveScreenshot`で各テストケースの入力後の状態のスクリーンショットが自動で保存されるようになっています。

# まとめ
CSVから値をセットしてみました。
毎回テストケースを書くよりは楽にテストできるようになるのではないでしょうか。
毎回CSVを読み込んだり、テストケース内で共通な部分も多くあるため、もっと作りこんで効率化することができそうですね。