【Vue3+TypeScript+Jest】Vue Test Utilsを用いた画面遷移のテスト

![Vue CLI-4.5.13](https://img.shields.io/badge/Vue CLI-4.5.13-<COLOR>) ![Vue-3.0.0](https://img.shields.io/badge/Vue-3.0.0-<COLOR>) ![TypeScript-4.1.5](https://img.shields.io/badge/TypeScript-4.1.5-<COLOR>) ![jest-24.0.19](https://img.shields.io/badge/jest-24.0.19-<COLOR>)

# はしがき

`Jest`で画面遷移のテストをしたくなったのですが、ドンピシャな記事がなかなか出てこなくてしんどかったので、ちょっと調べてサンプルを組んでみました。
フロントエンドのテストコードに取り組んで1日目なので、変なところがあるかもしれませんがご容赦ください。

# 目次
1. [前提](#前提)
2. [コード](#コード)
3. [コード解説](#コード解説)
4. [まとめ](#まとめ)

# 前提

[Vue CLI](https://cli.vuejs.org/)で生成したプロジェクトを使っています。
`Vue3`､`TypeScript`､`Jest`の3つが満たせていればよいので、それ以外の値は適当です。

### Vue CLIの実行
``` bash
$ vue create test-project

Vue CLI v4.5.13
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, TS, Router, Vuex, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? Yes
? Save preset as:
```

### プロジェクト構成（特に手は加えないです）

```
(root)
├── router
│   └── index.ts
├── views
│   ├── About.vue
│   └── Home.vue
├── App.vue
└── tests 
    └── unit
        └── example.spec.ts
```

# コード

完成系は以下の通りです。変更したファイルだけ載せています。

``` vue:views/Home.vue
<template>
  <router-link to="/about">Aboutヘ</router-link>
</template>
```

``` vue:App.vue
<template>
  <router-view />
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}
</style>
```

``` ts:example.spec.ts
import{ mount, flushPromises } from "@vue/test-utils";
import router from "@/router";
import Home from "@/views/Home.vue";
import About from "@/views/About.vue";
import { defineComponent } from "@vue/runtime-dom";

// 確認用の各画面のWrapper
const homeWrapper = mount(Home, {
  global: {
    plugins: [router],
  },
});
const aboutWrapper = mount(About);

describe("HelloWorld.vue", () => {
  it("Home.vueからAbout.vueへの遷移", async () => {
    // wrapperにHome.vueを表示させる（初期位置の設定）
    router.push("/");
    await router.isReady();

    // 実際にテストで動かす画面を作成
    const testApp = defineComponent({
      template: `<router-view />`,
    })
    const wrapper = mount(testApp, {
      global: {
        plugins: [router],
      },
    });

    // 遷移前の画面のHTMLがHome.vueと同一か確認
    expect(wrapper.html()).toBe(homeWrapper.html());

    // <a>タグ（今回は<router-link>しかない）を取得し、押下処理
    await wrapper.find("[data-test='router']").trigger("click");
    await flushPromises();

    // 遷移後の画面のHTMLがAbout.vueと同一か確認
    expect(wrapper.html()).toBe(aboutWrapper.html());
  });
});
```

# コード解説

公式ドキュメントの[Testing Vue Router](https://next.vue-test-utils.vuejs.org/guide/advanced/vue-router.html)に少し手を加えた程度のものになります。
`example.spec.ts`がメインなので、順に説明します。

### `Home.vue`、`About.vue`のWrapperを作成

実際に画面を動かすWrapperはテストの中で定義しますが、それとは別に静的に画面の情報（=HTML）を取得したいので、`Home.vue`と`About.vue`のWrapperを事前に作成しておきます。

``` ts
// 確認用の各画面のWrapper
const homeWrapper = mount(Home, {
  global: {
    plugins: [router],
  }
});
const aboutWrapper = mount(About);
```

VueWrapperの作成については[公式ドキュメント](https://next.vue-test-utils.vuejs.org/api/#global-plugins)を参照してください。  
`Home.vue`では`<router-link>`を使っているので、`<router-link>`を`<a>`タグに展開させるため、`router`を`global.plugin`マウントオプションで読み込む必要があります。

具体的に言うと以下の部分ですね。

``` ts
    plugins: [router], // ここでrouter/index.jsで定義されているRouterを読み込みます
```

`RouterLinkStub`を使う手もあるのですが、`RouterLinkStub`を使用した場合は`<router-link>`のまま出力されるため、今回はやりたいことに沿っておらず使わないことにしました。
（リンク先のテストなどでは`to`を使うことでコードを短くできるはずです）

### routerを活性化

今回は`router`で画面遷移を行うので、`router`が使えるようにしないといけません。
ただそのままだと使えないようなので、`router`の初期位置を指定したうえで、`router.isReady()`を使って`router`が使える状態になったか確認する必要があります。

> 参考：https://next.vue-test-utils.vuejs.org/guide/advanced/vue-router.html#using-a-real-router
>> Although it's not entirely clear from the warning, it's related to the fact that Vue Router 4 handles routing asynchronously.

今回は`Home.vue`から`About.vue`への遷移なので、`Home.vue（=/）`を初期位置に設定します。

``` ts
    // wrapperにHome.vueを表示させる（初期位置の設定）
    router.push("/");
    await router.isReady();
```

### 実際に動作させるWrapperを作成

`router`の準備ができたところで、動かすためのWrapperを作成します。
今回はテスト用に`<router-view />`だけ記載されているAppを作成して使うことにしました。

``` ts
    const testApp = defineComponent({
      template: `<router-view />`,
    })
    const wrapper = mount(testApp, {
      global: {
        plugins: [router],
      },
    });
```

画面遷移すると、この`<router-view />`が更新される感じですね。

### 画面遷移のテストを実施

これでテストの準備ができたので、実際にテストをしていきます。
`<router-link>`を押下した際に、`Home.vue`から`About.vue`に画面が切り替わっている状態を想定しているため、それの確認を行います。

``` ts
    // 遷移前の画面のHTMLがHome.vueと同一か確認
    expect(wrapper.html()).toBe(homeWrapper.html());

    // <a>タグ（今回は<router-link>しかない）を取得し、押下処理
    await wrapper.find("[data-test='router']").trigger("click");
    await flushPromises();

    // 遷移後の画面のHTMLがAbout.vueと同一か確認
    expect(wrapper.html()).toBe(aboutWrapper.html());
  });
```

`wrapper.html()`が`await wrapper.find("[data-test='router']").trigger("click");`の前後で変わるため、それを比較する感じですね。

`flushPromises`をしないと`router`の遷移が完了する前に次に行ってしまいます。

> 参考：https://next.vue-test-utils.vuejs.org/guide/advanced/vue-router.html#using-a-real-
>> Again, due to Vue Router 4's new asynchronous nature, we need to await the routing to complete before making any assertions.
>> 
>> In this case, however, there is no hasNavigated hook we can await on. One alternative is to use the flushPromises function exported from Vue Test Utils:

# まとめ

とりあえず`Jest`で画面遷移が確認できることが確認できたので良かったです。
わかってしまえば単純なのですが、前提知識がないためか`router`周りで苦労しました。

遷移先のパスが同一か、という記事は結構出てきたのですが、実ページまで確認するようなものはあまり見当たらなかったのですが、`Jest`で非推奨なのでしょうか？

サンプルはGithubに置いています。
https://github.com/kanade-nishikawa/vue-transition-test-sample

最後までありがとうございました。