Vueでよく書くユニットテストのパターン

Vueのコンポーネントのユニットテストでよく書くパターンを紹介します。
環境はvue-cliで生成したものをそのまま使っています。

APIの詳細な説明は[Vue Test Utils](https://vue-test-utils.vuejs.org/ja/api/)を参照してください。

## スナップショットテスト

出力されるHTMLが予期せず変更されないようにする場合に使うテストです。

``` GachaMv.spec.ts
it("itemsの値がhtmlに出力されているか？", () => {
  const items = [
    new Item("アイテム", 3, "アイテム説明")
  ];
  const wrapper = shallowMount(GachaMv, {
    propsData: { items }
  });
  expect(wrapper.html()).toMatchSnapshot();
});
```

上記のテストを追加し`npm run test:unit` or `yarn test:unit`を実行するとテストファイルと同階層に`__snapshots__/GachaMv.spec.ts.snap`が出力されます。

```html:GachaMv.spec.ts.snap
exports[`GachaMv.vue itemsの値がhtmlに出力されているか？ 1`] = `
<div class="gacha-mv">
  <slick-stub options="[object Object]">
    <div class="gacha-mv-list">
      <p class="gacha-mv-list-name">アイテム</p>
      <p class="gacha-mv-list-rare">3</p>
      <p class="gacha-mv-list-description">アイテム説明</p>
    </div>
  </slick-stub>
</div>
`;
```

shallowMountではなくmountを使用した場合は、以下のように子コンポーネントも展開した状態で出力されます。

```html:GachaMv.spec.ts.snap
exports[`GachaMv.vue itemsの値がhtmlに出力されているか？ 1`] = `
<div class="gacha-mv">
  <div class="slick-initialized slick-slider">
    <div class="slick-list draggable">
      <div class="slick-track" style="opacity: 1; width: 0px; transform: translate(0px, 0px);">
        <div class="slick-slide slick-current slick-active" data-slick-index="0" aria-hidden="false" style="margin-left: 0px; width: 0px;">
          <div>
            <div class="gacha-mv-list" style="width: 100%; display: inline-block;">
              <p class="gacha-mv-list-name">アイテム名</p>
              <p class="gacha-mv-list-rare">3</p>
              <p class="gacha-mv-list-description">アイテム説明</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
`;
```

以下のようにshallowMountとstubsを組み合わせることで、一部のコンポーネントのみスタブすることも可能です。

```js:GachaMv.spec.ts.snap
const wrapper = shallowMount(GachaMv, {
  stubs: {
    'slick': Slick,
  },
}
```

## computedが期待した値を返しているか？

computedが正しい値を確認しているかのテストです。

``` GachaMv.spec.ts
it("pickupItemが期待通りか？", async() => {
  const items = [
    new Item("アイテム名3", 3, "アイテム説明3"),
    new Item("アイテム名4", 4, "アイテム説明4"),
    new Item("アイテム名5", 5, "アイテム説明5"),
  ];
  const wrapper = shallowMount(GachaMv, {
    propsData: { items }
  });
  expect((wrapper.vm as any).pickupItems).toEqual([new Item("アイテム名5", 5, "アイテム説明5")]);
});
```

## クリックイベントが動作しているか？

要素がイベントを発火した時に、定義した関数が実行されているかのテストです。
スタブにsinonを使ってます。

``` GachaPlay.spec.ts
it(".gacha-playクリック時にonPlayGachaが実行されるか？", () => {
  const onPlayGachaStub = sinon.stub();
  const wrapper = shallowMount(GachaPlay, {
    propsData: { 
      number: 1,
      onPlayGacha: onPlayGachaStub
    }
  });
  wrapper.find(".gacha-play").trigger('click');
  expect(onPlayGachaStub.called).toBe(true);
  onPlayGachaStub.restore();
});
```

### sinon or jest.fn

先ほどのクリックイベントのテストでsinonを使いましたが、jest.fnでも同じことができます。
個人的は使い慣れているsinonを使用しています。

[unit testing - stubbing a function using jest - Stack Overflow](https://stackoverflow.com/questions/45259086/stubbing-a-function-using-jest)

## 非同期のテスト

非同期テストでは、flush-promisesやVue.nextTickを使わないと期待した結果が得られません。

### flush-promisesについて

[flush-promises](https://github.com/kentor/flush-promises/blob/master/index.js)

> 保留中の解決済みの約束のハンドラをすべてフラッシュします。

やっていることはmicrotasks または macrotasksのプロミスを返しているだけです。（setImmediate関数がある場合はmicrotasksが選択されるので、これがDOMの更新を待たなかった原因かもしれません。）  
microtasks、macrotasksについは以下の記事が分かりやすかったです。


[Tasks, microtasks, queues and schedules - JakeArchibald.com](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)

### Vue.nextTickについて
[Vue.nextTick](https://jp.vuejs.org/v2/api/index.html#Vue-nextTick)

> callbackを延期し、**DOMの更新サイクル後**に実行します。DOM更新を待ち受けるために、いくつかのデータを変更した直後に使用してください。


### テスト記述例

VueのDOMの更新後の値をテストする場合はnextTick、非同期処理(Promiseをfulfilled)する場合はflushPromisesを使用します。

```js
it('test', async() => {
  const wrapper = shallowMount(Foo);
  await flushPromises(); // マウント時などに非同期処理がある場合
  expect(wrapper.vm.text).toBe('マウント時のテキスト');
  // expect(wrapper.find('text').text()).toBe('マウント時のテキスト'); // DOMが更新される保証はないので、Errorになるかも

  wrapper.find('button').trigger('click'); // クリックイベントで非同期処理を実行

  await wrapper.vm.$nextTick(); // DOMの更新を保証
  expect(wrapper.find('text').text()).toBe('クリック後のテキスト');
})
```

## さいごに

簡単なコンポーネントのテストであれば、これらの組み合わせで十分かと思います。
今回のユニットテストではUIの崩れなど検知できないので、以下のリンク先で紹介されているようなstorybookやvisual regressionテストなどの導入を検討してみてください。

[Storybookとvue-i18nで多言語確認を容易にしよう - スタディスト開発ブログ - Medium](https://medium.com/studist-dev/internationalization-in-storybook-4be77773494c)

[Storybookとreg-suitで気軽にはじめるVisual Regression Testing - wadackel.me](https://blog.wadackel.me/2018/storybook-chrome-screenshot-with-reg-viz/)