VueのUIテストにStorybookを使う

# 例

以下を実施するうえでベースになりそうな設定をしたもの
https://github.com/sterashima78/vue-storybook-unittest

# 動機

Componentの試験書いていたのですが、Templateに相当する部分の試験はどうしようかと思っていました。
機能的な観点で考えれば vue-test-utilを駆使して、あるエレメントをクリックしたら、あるメソッドがコールされるなどの試験でよさそうな気がします。（私はこれが嫌いなので採用していませんがこの記事には関係ないので深く突っ込みません）
しかし、これはイベントハンドリングという一面に過ぎずUIの責任はこれにとどまりません。
どのような表示になるか、どのようにレンダリングされるべきかという点をうまく確認できるようにしたいところです。

コンポーネントカタログとしてStoryBookがあります。
これを使えばコンポーネント単位での表示の妥当性が確認できそうです。
実際プロジェクトで利用しているのですが、以下の問題を感じました。

- StoryBookを維持する動機を持ち続けるのが難しい
- どういう観点でStoryを作るべきかが開発者によってぶれる

前者についてですが、これは主にコードを触る人間が「機能」に対して興味を持った結果、Unit test の面倒を見るのですが、StoryBookにはそれほど関心は持たず、ビルドもできてるし問題ないかと思っていたら、 Props が変更されたりしてStoryBookが機能しないみたいなことがありました。

後者に関してですが、例えば Knobs を利用して依存する Props をすべて可変にして一つのストーリだけを定義するみたいな使われ方がされました。個人的にはそのコンポーネントのユースケース = Story みたいな認識ですが、（あんまり興味を持っていない）StoryをDRYに書きたいという欲求からこうなってしまったと認識しています。

# 方針

## Storyは単一のテストケースである

VueはWebアプリケーションの View レイヤを担当しますが、この View は Component へのパラメータ (props) と 内部状態 (data) で決定します。
Storyでは UI(つまりView) の確認（検証）をしたいのでこの二つの要素をパターンを十分に網羅した Story (テストケース) を用意すればいいと考えることができます。
これは、 `jest` の `test.each` を使った table driven なテストと同じです。

=> Storyは単一のテストケースであるととらえることで、「どういう観点でStoryを作るべきかが開発者によってぶれる」という点を解決

## Storyごとに SnapShot test (DOM) と Visual Regression test をする

Story をテストケースととらえているので、自動的に適切な検証がしたいです。
ここではよくつかわれている SnapShot test と Visual Regression testをする方向で考えます。

=> Storyごとにテストを実行することで、CIを通すためにメンテが必須になります。これで「StoryBookを維持する動機を持ち続けるのが難しい」という問題が解決

それぞれ以下の役割を想定しています。

### SnapShot test

早いテストという位置づけです。CIでは先に流して落としてもらうことになると思っています。
なので、ここに求めるのは早いフィードバック・現状のコードの問題に関する情報です。
主に以下の情報を提供してもらいます。

- デグレの可能性
- テストケースの網羅性

前者は当たり前というか特に説明をしなくてもわかると思います。
後者についてですが、SnapShot test の実行時にコードカバレッジをとります。
v-if などで利用する評価式などを computed で実装しておけば、カバレッジレポートを確認することで、
Story (テストケース)の漏れに気が付く材料になります。

本当は Template に対するカバレッジがわかればいいのですが Vue の SFC では無理なようです（JSXでは可能なのですか？）。

### Visual Regression test

遅いテストという位置づけです。
SnapShot testに比べて多くの視覚的情報が得られます。
SnapShot testに通った上で意図したUIになっているかどうかの検証に漏れがないようにします。

# 作業

例に示したリポジトリを作るまでで主要な箇所です。

## 必要なものの作成・インストール

```console
$ vue create <project>
# TypeScriptを選択して Class Componentは選択していません
# また、Unit testでランナーを jest にしています。
$ cd project
$ vue add storybook
$ npm install --save-dev \
    @storybook/addon-storyshots-puppeteer \
    @storybook/addon-storyshots \
    @types/storybook__addon-storyshots \
    @types/storybook__addon-storyshots-puppeteer \
    babel-plugin-require-context-hook
```

## jest で `require.context` を実行するために以下を設定

- `jest.config.js` を修正
    - `transform` に `"^.+\\.(js|jsx)?$": "babel-jest",` を追記
    - `setupFiles` に `["./tests/setup.js"],` を設定
- `./tests/setup.js` に以下を記述

    ```js
    import registerRequireContextHook from "babel-plugin-require-context-hook/register";
    registerRequireContextHook();
    ```
- `babel.config.js` を修正
    - `env` に以下を設定

```js
    test: {
      plugins: ["require-context-hook"]
    }
```

## SnapShot test


```typescript:src/stories/__tests__/dom.test.ts
import initStoryshots from "@storybook/addon-storyshots";
import path from "path";
initStoryshots({
  configPath: path.resolve(__dirname, "..", "..", "..", "config", "storybook")
});

```

以下で実行させる

```sh
vue-cli-service test:unit src/stories/__tests__/dom.test.ts --coverage --coverageDirectory=coverage-snapshot
```

## Visual Regression test

```typescript:src/stories/__tests__/image.test.ts
import initStoryshots from "@storybook/addon-storyshots";
import { imageSnapshot } from "@storybook/addon-storyshots-puppeteer";
import path from "path";

initStoryshots({
  suite: "Image storyshots",
  test: imageSnapshot({
    storybookUrl: `file://${path.resolve(
      __dirname,
      "..",
      "..",
      "..",
      "storybook-static"
    )}`
  }),
  configPath: path.resolve(__dirname, "..", "..", "..", "config", "storybook")
});


```

以下で実行させる

```sh
vue-cli-service test:unit src/stories/__tests__/image.test.ts
```

# まとめ

StoryBook の位置づけを明確にして、CIパイプラインに組み込むことを踏まえた構成を提案した。
結果、腐らず開発を推進するようなStoryBook運用が期待できる。