Vue3 のテストを VTU(Vue Test Utils) と Jest で実施するに当たり環境構築から実際にテストするまでに詰まりまくったのでまとめたいと思います。
環境構築編
Vue3 に対応しているのが VTU2系のためこちらをインストールしてテストランナーである Jest でテスト走らせるというのをやっていきます。
VTU2 : https://next.vue-test-utils.vuejs.org/installation/
Jest : https://jestjs.io/
結論
最初に結論を申し上げますが、各ライブラリの依存関係がとても難しいので、慣れてる人でない限り公式で用意されているデモを元に環境構築したほうが無難です。
私はこの環境構築に半日以上費やして、結局うまく行きませんでした 😵
公式サイトのデモ : https://github.com/lmiller1990/vtu-next-demo
ES6でプロジェクト進めている方は Jest がサポートしていないので、別に Babel というコンパイラを入れる必要があります。
Babel : https://babeljs.io/
私は ES6 で書いていたので、VTU2, Jest, Babel その他にも細かなライブラリを入れる必要があり、慣れている人でないと恐らく難しいです。。。
とりあえずこれで動くよ!
2022年1月8日現在ならこれでとりあえず動きます!自分の備忘録のためにも記載しておきます。
package.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
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
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
},
],
],
};
テスト実施編
さぁ、これでテストが動くようになったわけですが、実際にテストやってみてさらに詰まる。
テスト基本
以下のコンポーネントは公式サイトのもの引用しています。
<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>
上記のコンポーネントをテストするには以下のようになります。こちらも公式サイトのもの引用です。
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')
})
なにやってるかざっくり申し上げますと
-
mount(TodoApp)
でTodoApp
コンポーネントをマウントしたものをwrapper
に入れる。 -
wrapper.get
で DOM を取得(find
的なことしてます)。 -
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
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
最終的なコード
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 にアクセスできるようになりました!