Data Flow Driven Testで複雑なUIのテストを簡潔に書こう

## はじめに

E2Eテストコードの保守性について論じるとき、特に話題になるのは[Page Object Pattern](https://martinfowler.com/bliki/PageObject.html)などによるテスト対象ソフトウェアの抽象化が中心で、テストシナリオそのものの保守性については意外に話題に上がることが少ないように思います。
ですが、言うまでもないことですがテストシナリオの見通しを良くすることは非常に重要で、特に現代の複雑なUI・フローを持つアプリケーションにおいては、テストシナリオの保守性について考えることが、自動テストそのものの成否を握っていると言っても過言ではありません。

そんなわけで、複雑なUIフローのテストをシンプルに記述するための技法として、 **Data Flow Driven Test** というものを編み出したので、ご紹介しようと思います。

## Data Flow Driven Testとは

平たく言うと、「データドリブンテストで、データだけでなく操作（step）も渡す」ことです。
データドリブンテストについては[ググってもらったほうが早い](https://en.wikipedia.org/wiki/Data-driven_testing)と思いますが、ウルトラ雑な説明をすると、 *テーブル状のデータ（Excel的なやつ）をもとに、異なるパラメータでテストを行う* ことです。

## Data Flow Driven Testの具体例

### 辛い例

例えば、アカウント登録のUIがあるとします。

- 国内/海外の区分がある
    - 海外の場合は国名の入力が必須
- 法人/個人の区分がある
- 住所、氏名、電話番号の登録は共通
- 法人の場合は以下の入力が必要
    - 法人番号
        - ただし海外の場合は入力不要
    - 法人名
- 個人の場合は以下の入力が必要
    - 生年月日

整理するとこんな感じのマトリクスができそうですね。

| 区分 | 国名 | 住所 | 氏名 | 電話番号 | 法人名 | 法人番号 | 生年月日 |
|:--|:--|:--|:--|:--|:--|:--|:--|
| 国内/法人 | ☓ | ◯ | ◯ | ◯ | ◯ | ◯ | ☓ |
| 国内/個人 | ☓ | ◯ | ◯ | ◯ | ☓ | ☓ | ◯ |
| 海外/法人 | ◯ | ◯ | ◯ | ◯ | ◯ | ☓ | ☓ |
| 海外/個人 | ◯ | ◯ | ◯ | ◯ | ☓ | ☓ | ◯ |

これをデータドリブンテストで書こうとすると、こんな感じのデータを用意することになりそうです。

| 所在地 | 事業区分 | 国名 | 住所 | 氏名 | 電話番号 | 法人名 | 法人番号 | 生年月日 |
|:--|:--|:--|:--|:--|:--|:--|:--|:--|
| 国内 | 法人 | ☓ | 東京都千代田区千代田1-1-1 | QA 太郎 | 000-0000-0000 | 株式会社ホワイト企業 | 9-999-999 | ☓ |
| 国内 | 個人 | ☓ | 東京都千代田区千代田1-1-1 | QA 太郎 | 000-0000-0000 | ☓ | ☓ | 1987-03-16 |
| 海外 | 法人 | USA | Washington, Dokoka | QA 太郎 | +00-0000-0000 | White Company Inc. | ☓ | ☓ |
| 海外 | 個人 | USA | Washington, Dokoka | QA 太郎 | +00-0000-0000 | ☓ | ☓ | 1987-03-16 |

ここまではすっきりしていて良いのですが、これを渡された実装側はどうなるかというと、こうなります。
（サンプルコードは [CodeceptJS](https://codecept.io) で書いています）

```javascript

/**
 * `data` 変数にCSVの行が配列で格納されています
 *     const data = [
 *        {
 *            '所在地': '国内',
 *            '事業区分': '個人',
 *            ...
 *        },
 *        {
 *            '所在地': '国内',
 *            '事業区分': '法人',
 *        },
 *        ...
 *     ] 
 */

/**
 *  CodeceptJSのData Driven Testsの記法について、詳しくは下記URLを参照してください
 *  https://codecept.io/advanced#data-driven-tests
 *
 *  `Data(data)` でIteratableな値を渡して、 `current` に現在のデータが入る感じになります
 */
Data(data).Scenario('アカウント登録のテスト', (I, current) => {
    I.amOnPage('/user/account')  // アカウント登録ページにアクセス
    I.checkOption(current['所在地'])
    I.checkOption(current['事業区分'])
    if (current['所在地'] === '海外') {
        I.fillField('国名', current['国名'])
    }
    I.fillField('住所', current['住所'])
    I.fillField('氏名', current['氏名'])
    I.fillField('電話番号', current['電話番号'])
    if (current['事業区分'] === '法人') {
        I.fillField('法人名', current['法人名'])
        if (current['所在地'] === '国内') {
            I.fillField('法人番号', current['法人番号'])
        }
    }
    if (current['事業区分'] === '個人') {
        I.fillField('生年月日', current['生年月日'])
    }
})
```

この実装の問題点は2つあります。

- 条件分岐が既に辛くなってきており、要件の追加で容易に地獄行きになることが目に見えている
- CSV側の入力を無視される可能性がある（例えば、事業区分を `個人` にしているのに `法人番号` を入力してしまった、などの異常系はテストできない）

### Data Flow Driven Testによるリファクタリング

まずは共通部分だけを切り出してみます。

```javascript

I.amOnPage('/user/account')  // アカウント登録ページにアクセス
I.checkOption(data['所在地'])
I.checkOption(data['事業区分'])
I.fillField('住所', data['住所'])
I.fillField('氏名', data['氏名'])
```

次に、 `if` で分岐していた部分を、区分ごとに整理してみます。

```js
// 国内・個人
I.fillField('生年月日', data['生年月日'])

// 国内・法人
I.fillField('法人名', data['法人名'])
I.fillField('法人番号', data['法人番号'])

// 海外・個人
I.fillField('生年月日', data['生年月日'])
I.fillField('国名', data['国名'])

// 海外・法人
I.fillField('国名', data['国名'])
I.fillField('法人名', data['法人名'])
```

次に、これらをデータドリブンテストのデータの一部として追加します。
具体的には、データオブジェクトに `option` というプロパティを追加して、そこに無名関数でラップしたステップを代入します。
また、データの中のステップには**直接入力したい値を記述**します。これは、データドリブンテストの `データ` と、それをどのように扱うかという `フロー` を合わせたものをシナリオに渡すという性質上、このようにしています（ `Data Flow Driven` と呼ぶゆえんです）。

```js
const data = [
    {
        '所在地': '国内',
        '事業区分': '個人',
        // (中略)
        option: () => {
            // data['生年月日'] のような記述ではなく、値を直接入力している点に注目
            I.fillField('生年月日', '1987-03-16')
        },
    },
    {
        '所在地': '国内',
        '事業区分': '法人',
       // (中略)
        option: () => {
            I.fillField('法人名', '株式会社ホワイト企業')
            I.fillField('法人番号', '9-999-9')
        },
    },
    {
        '所在地': '海外',
        '事業区分': '個人',
       // (中略)
        option: () => {
            I.fillField('生年月日', '1987-03-16')
            I.fillField('国名', 'USA')
        },
    },
    {
        '所在地': '海外',
        '事業区分': '法人',
       // (中略)
        option: () => {
            I.fillField('国名', 'USA')
            I.fillField('法人名', 'White Company Inc.')
        },
    },
]
```

最後に、テストシナリオを修正します。

```js
Data(data).Scenario('アカウント登録のテスト', (I, current) => {
    I.amOnPage('/user/account')  // アカウント登録ページにアクセス
    I.checkOption(current['所在地'])
    I.checkOption(current['事業区分'])
    I.fillField('住所', current['住所'])
    I.fillField('氏名', current['氏名'])
    I.fillField('電話番号', current['電話番号'])
    current.option()  // ここでフローを実行する
})
```

どうでしょう、テストシナリオからIf分岐が消えて、見通しが良くなったと思いませんか？

## おわりに

もともとこれを思いついた発端として、現代の複雑な要件・UIの上では、単純なデータドリブンテストは役に立たないことが多く、ステップそのものを柔軟に差し替えられる手段が欲しい、と思ったのがきっかけでした。

データドリブンテストの役割というのは `一つの観点を異なるテストデータでテストする` ということです。
素朴なやりかただと、それは例えば同値分割や境界値などをテストするのに使うのですが、それが単にパラメータとして使えるのはユニットテストまでで、UIテストやE2Eテストの文脈では `値が変わればUIも変わる` のが普通です。
入力値によって変化する複雑なUIを、データドリブンテストの考え方でシンプルに記述するにはどうすれば良いのか、という問題に対する一つの答えとして受け取っていただければ幸いです。

あと、なんか書いてるうちに考えとしてはすごくありふれたもののような気がしてきたので、実は正しい名前があるのかもしれないと思ってきました。ご存知の方はこっそり教えて下さい。

## 超余談

この記事はもともと[OPENLOGI Advent Calendar 2019](https://qiita.com/advent-calendar/2019/openlogi)に書く予定で夏ぐらいから準備していたものですが、うっかり転職してしまったので叶いませんでした。ごめんね。