Vue3, VTU, Jestで Composition API テストを導入

Vue3 のテストを VTU(Vue Test Utils) と Jest で実施するに当たり環境構築から実際にテストするまでに詰まりまくったのでまとめたいと思います。

## 環境構築編

Vue3 に対応しているのが VTU2系のためこちらをインストールしてテストランナーである Jest でテスト走らせるというのをやっていきます。

VTU2 : [https://next.vue-test-utils.vuejs.org/installation/](https://next.vue-test-utils.vuejs.org/installation/)
Jest : [https://jestjs.io/](https://jestjs.io/)

### 結論

最初に結論を申し上げますが、各ライブラリの依存関係がとても難しいので、慣れてる人でない限り公式で用意されているデモを元に環境構築したほうが無難です。

私はこの環境構築に半日以上費やして、結局うまく行きませんでした 😵

公式サイトのデモ : [https://github.com/lmiller1990/vtu-next-demo](https://github.com/lmiller1990/vtu-next-demo)

ES6でプロジェクト進めている方は Jest がサポートしていないので、別に Babel というコンパイラを入れる必要があります。

Babel : [https://babeljs.io/](https://babeljs.io/)

私は ES6 で書いていたので、VTU2, Jest, Babel その他にも細かなライブラリを入れる必要があり、慣れている人でないと恐らく難しいです。。。

### とりあえずこれで動くよ！

2022年1月8日現在ならこれでとりあえず動きます！自分の備忘録のためにも記載しておきます。

package.json

```json
"@vue/test-utils": "^2.0.0-alpha.8",
"@babel/preset-env": "^7.9.0",
"@types/jest": "^25.2.1",
"babel-jest": "^25.2.6",
"jest": "^25.2.7",
"ts-jest": "^25.3.1",
"vue-jest": "^5.0.0-alpha.1",
```

jest.config.js

```jsx
module.exports = {
  preset: 'ts-jest',
  globals: {},
  testEnvironment: 'jsdom',
  transform: {
    "^.+\\.vue$": "vue-jest",
    "^.+\\js$": "babel-jest"
  },
  moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node']
}
```

babel.config.js

```jsx
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        targets: {
          node: "current",
        },
      },
    ],
  ],
};
```

## テスト実施編

さぁ、これでテストが動くようになったわけですが、実際にテストやってみてさらに詰まる。

### テスト基本

以下のコンポーネントは公式サイトのもの引用しています。

```jsx
<template>
  <div>
    <div v-for="todo in todos" :key="todo.id" data-test="todo">
      {{ todo.text }}
    </div>
  </div>
</template>

<script>
export default {
  name: 'TodoApp',

  data() {
    return {
      todos: [
        {
          id: 1,
          text: 'Learn Vue.js 3',
          completed: false
        }
      ]
    }
  }
}
</script>
```

上記のコンポーネントをテストするには以下のようになります。こちらも公式サイトのもの引用です。

```jsx
import { mount } from '@vue/test-utils'
import TodoApp from './TodoApp.vue'

test('renders a todo', () => {
  const wrapper = mount(TodoApp)

  const todo = wrapper.get('[data-test="todo"]')

  expect(todo.text()).toBe('Learn Vue.js 3')
})
```

なにやってるかざっくり申し上げますと

1. `mount(TodoApp)` で `TodoApp` コンポーネントをマウントしたものを `wrapper` に入れる。
2. `wrapper.get` で DOM を取得（ `find` 的なことしてます）。
3. `todo.text()` で取得した DOM のテキストを出力

となります。

Vue の Unit テストではこのように「**何が出力されているか**」をテストしたりします。

### 問題点

**DOMが取得できない**

先程の例でいうと `todo.text()` をすると `Cannot call text on an empty DOMWrapper.` ってエラーが出ました。


これが本当によくわかりませんでした。

いろいろ調べた結果2つの要素がエラーの原因でした。

**原因①**

「テストしているコンポーネントが Async コンポーネントだった」

業務中に出たエラーなので詳しくコードは書けませんが、Composition Api の `<script lang="ts" setup>` 構文を使用しており、 Async コンポーネントになっていました。

Github の issue にあがっていましたが、 Async コンポーネントには `<Suspense>` でコンポーネントを囲って挙げないといけないようです。

issue : [https://github.com/vuejs/vue-test-utils-next/issues/1207#issuecomment-1004825195](https://github.com/vuejs/vue-test-utils-next/issues/1207#issuecomment-1004825195)

```jsx
test('Suspense', () => {
    const Component = defineComponent({
      components: { Async },
      template: '<Suspense><Async/></Suspense>'
    })

    const wrapper = mount(Component)
   // ...
  })
```

この間出た issue みたい。これなかったらマジで詰んでました。。。

**原因②**

「DOMのマウントが終わっていない状態だった」

DOM の生成が終わる前にアクセスしてしまっているため、async コンポーネントは処理が終わったことを担保しないとアクセスできないようです。

（参考にした issue あったのですがちょっと見つかりませんでした。すみません。） 

処理の終了を担保するために `flushPromise()` というメソッドを使用しました。

[https://next.vue-test-utils.vuejs.org/api/#flushpromises](https://next.vue-test-utils.vuejs.org/api/#flushpromises)

**最終的なコード**

```jsx
it("display Hello World", async () => {
    const Component = defineComponent({
      components: { TestComponent },
      template: "<Suspense><TestComponent/></Suspense>",
    });

    wrapper = mount(Component);
    expect(wrapper).toBeTruthy();
    expect(wrapper.exists()).toBe(true);

    await flushPromises();
		// ここからアクセスできるようになる
    const h1El = wrapper.find("h1");
    expect(h1El.text()).toBe("h1text");

  });
```

上記で DOM にアクセスできるようになりました！