Jest で Vue.js を単体テストしよう

`vue create` で生成したプロジェクトで typescript と Jest を選択しているプロジェクトで単体テストをしてみましょう。といっても既に `tests/unit/example.spec.ts` に以下のようなコードが生成されています。

```typescript
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})
```

お膳立てが出来ているって素晴らしい。

## テスト用コンポーネントの作成

ということで、ちょっと頑張って以下のようなコンポーネントを作ってみました。

```vuejs
<template>
  <div>
    <div v-if="isEmpty">
      データがありません
    </div>
    <table class="content" v-else>
      <thead>
        <tr>
          <th>Date</th>
          <th>Temperature C</th>
          <th>Summary</th>
        </tr>
      </thead>
      <tbody>
        <tr :key="index" v-for="(f, index) in store.forecasts">
          <td>{{ f.date }}</td>
          <td>{{ f.temperatureC }}</td>
          <td>{{ f.summary }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import store from '@/store';
export default Vue.extend({
  data() {
    return {
      store,
    };
  },
  async created() {
    await store.loadWeatherForecasts();
  },
  computed: {
    isEmpty(): boolean {
      return this.store.forecasts.length == 0
    },
  },
});
</script>
<style>
table.content {
  margin-right: auto;
  margin-left: auto;
}
</style>
```

ポイントは、このコンポーネントは store という得たいの知れない別ファイルに依存していて、そいつはどうも loadWeatherForecasts という非同期メソッドを created で呼んでいるみたいだということです。

おそらく、そのメソッドの呼び出しが終わると store.forecasts の配列にデータが入っていて、それを元にテーブルを描画してるんだなというのがわかります。データが空のときはデータが無いという旨のメッセージを出していますね。

実際のコードでは loadWeatherForecasts は Swagger から生成した TypeScript 用の API のクライアントコードを使ってます。forecasts は、その戻り値の型の配列です。

じゃぁテスト作ってみましょう。

## テスト作成

shallowMount で Forecast.vue で定義されたコンポーネントをマウントします。shallow という名前の通り子コンポーネントは純粋にスタブになっている点が単体テスト向きです。一応 shallow じゃない mount も用意されてますが、子コンポーネントの描画も含めてテストしたいときは使う感じですが、単体テストとしてなら shallowMount のほうが適切な感じですね。

さくっとやってみましょう。モックの差し込み方とかは 1 つ前の記事でやった方法で OK です。

[TypeScript の単体テストで Jest 使おう](https://qiita.com/okazuki/items/991a068892e946531612)

```typescript
import { shallowMount } from '@vue/test-utils';
import { api } from '@/api';
import store from '@/store';
import Forecasts from '@/views/Forecasts.vue';

jest.mock('@/store');

const mockStore = store as jest.Mocked<typeof store>;

describe('Forecasts.vue', () => {
    test('データが空のときはテーブルを描画しない', () => {
        mockStore.forecasts = [];
        const forecasts = shallowMount(Forecasts);

        expect(mockStore.loadWeatherForecasts).toHaveBeenCalled();
        expect(forecasts.text()).toMatch('データがありません');
        expect(forecasts.find('table').exists()).toBe(false);
    });

    test('データがあるときはテーブルが描画される', async () => {
        mockStore.loadWeatherForecasts.mockImplementation(() => {
            mockStore.forecasts = [ 
                new api.WeatherForecast({ date: new Date(2020, 5, 1, 0, 0, 0), temperatureC: 0, temperatureF: 10, summary: 's1' }),
                new api.WeatherForecast({ date: new Date(2020, 5, 2, 0, 0, 0), temperatureC: 1, temperatureF: 20, summary: 's2' }),
                new api.WeatherForecast({ date: new Date(2020, 5, 3, 0, 0, 0), temperatureC: 2, temperatureF: 30, summary: 's3' }),
            ];
            return Promise.resolve();
        });

        const forecasts = shallowMount(Forecasts);
        const tableRows = forecasts.findAll('table > tbody > tr');

        expect(mockStore.loadWeatherForecasts).toHaveBeenCalled();
        expect(tableRows.length).toBe(3);
        expect(tableRows.at(0).text()).toBe('Mon Jun 01 2020 00:00:00 GMT+0900 (GMT+09:00) 0 s1');
        expect(tableRows.at(1).text()).toBe('Tue Jun 02 2020 00:00:00 GMT+0900 (GMT+09:00) 1 s2');
        expect(tableRows.at(2).text()).toBe('Wed Jun 03 2020 00:00:00 GMT+0900 (GMT+09:00) 2 s3');
    });
});
```

さくっとグリーンになりました。JavaScript 系のテストはモックを差し込みやすいポイントとしては import になるので、きちんとファイル分割して import するように作ってれば単体テストのときにつぶしが効きそうですね。

## その他知っておいた方がよさそうなやつ

正直公式のガイドが凄くよく書かれています。しかも日本語で（翻訳してくれている人に大感謝）

[Vue Test Utils | ガイド](https://vue-test-utils.vuejs.org/ja/guides/)

### `createLocalVue` 関数

テスト用の Vue インスタンスを作ってくれます。

```typescript
// ローカルの Vue インスタンス
const localVue = createLocalVue();
// 色々設定（ここでは VueRouter）
localVue.use(VueRouter);
const router = new VueRouter();

// ローカルの Vue インスタンスを使ってマウントする
const testTarget = shallowMount(SomeComponent, {
  localVue,
  router,
});
```

Vue インスタンスは何も考えないとグローバルで 1 つなので、別のテストで汚染された Vue インスタンスではなく、個々のテストで Vue インスタンスを作るってことですかね。

### Vuex のテスト

Vuex も基本的にはモックをうまく使ってテストです。ガイドに書いてある通りでよさそう。

### 非同期系

ガイドで紹介されている `flush-promises` がとっても便利そう。全ての resolve されている Promise のハンドラーを実行してくれる。

使い方も `await flushPromises();` をするだけでシンプル。便利そう。

### ボタンを押したりしたい

trigger メソッドをガイドで探せば OK。

### イベントの発行の確認

shallowMount の戻り値に対して `emitted()` を呼ぶと結果が返ってくるので、これをアサートすれば OK

## まとめ

公式で日本語で書いてあるのマジ神。


## まとめ

単体テストしよう。