Vue-test-utilsのshallowMountとmountの違いについて
この記事では Vue-test-utils の shallowMount
と mount
の違いについてをメインに記事にしていきます。また shallowMount
+ stubs
についても書いていきたいと思います。 Component.methods
とコンポーネントから直接 methods
を呼び出す方法もあるのでよかったら見ていってください。
mount
shallowMount
shallowMount + stubs
TodoContainer.methods
このあたりでを書いていきたいと思います。
個人的には「Vue-test-utils」には、Vue.jsのコンポーネントとかをいい感じにwrapしてくれて、jestとかのテストランナーに乗せれるようにしてくれるいい感じのヤツという認識があります。(訂正とか正確な表現があればコメント欄なりPRお願いします。)
Vue-test-utilsがどんなものか、どういう感じで誕生したかは、以下の記事に詳しいかと思います。 vue-test-utilsを使用してテストを書いてみた(Vue.js)
ドキュメントは https://vue-test-utils.vuejs.org/ja/ こちらになります。今回の内容は、ドキュメントに書いてあることがほとんどなので、私の記事からテストに入った方もぜひドキュメントを読んでみてください。このドキュメントは長くないので流し読みなら1時間もあれば読めると思います。
はじめに
こんにちは。僕です。
前回は 【Vue.js】いつから「フロントエンド開発でTDDができない」と錯覚していた? ※1 という記事を書かせていただきました。。フロントエンド界隈ってまったくテストが盛り上がっていなくて、ましてや「弊社TDDやってます」なんという声とかはほとんどあがっていない状況です。(私はそう感じているという主観的な意見ですが。)
※1 タイトルの元ネタ BLEACHの藍染惣右介のセリフより
「フロントエンドのテストって難しくないよ!!」とか「フロントエンドのロジック部分はTDDできるよ!!」って伝えたくて記事を書かせていただきました。基本的にVueのテストについての記事はQiitaに書いていくので、QiitaかTwitterをフォローしてくれると情報が早いかと思います。
本文
前回のコンポーネントの復習
前回の 記事 を読んでいないかたはぜひ読んできてください。同じコンポーネントを使うことにします。
$ git clone https://github.com/ykhirao/vue-todos.git
<template>
<div>
<NewTodo
@createTodo="addTodo"
></NewTodo>
<TodoItem
v-for="todo in todos"
:id="todo.id"
:key="todo.id"
:text="todo.text"
:checked="todo.checked"
@toggleChecked="toggleChecked"
></TodoItem>
</div>
</template>
<script>
import NewTodo from "@/components/NewTodo.vue"
import TodoItem from '@/components/TodoItem.vue'
export default {
name: "todo-container",
components: {
NewTodo,
TodoItem
},
data() {
return {
todos: []
}
},
methods: {
addTodo(text) {
this.todos.push({text, checked:false, id: this.todos.length + 1 })
},
toggleChecked(id) {
const todo = this.todos.find(x => x.id === id)
todo.checked = !todo.checked
}
}
}
</script>
<template>
<div>
<input
type="radio"
class="radio"
:checked="checked"
@click="toggleChecked"
>
<span>{{text}}</span>
</div>
</template>
<script>
export default {
name: "todo-item",
props: {
text: {
type: String,
required: true
},
checked: {
type: Boolean,
required: true
},
id: {
type: Number,
required: true
}
},
methods: {
toggleChecked() {
this.$emit("toggleChecked", this.id)
}
}
}
</script>
<template>
<div>
<input
class="new"
v-model="text"
@keyup.enter="submit"
>
</div>
</template>
<script>
export default {
name: "new-todo",
data() {
return {
text: ""
}
},
methods: {
submit() {
this.$emit("createTodo", this.text)
}
}
}
</script>
htmlの比較
さて
html methodについて
shallowMountのhtml()
it.only("toggleChecked", () => {
const wrapper = shallowMount(TodoContainer, {
data() {
return {
todos: [{ id: 1, text: "text", checked: true }]
}
}
})
console.log(wrapper.html())
})
<div>
<new-todo-stub></new-todo-stub>
<todo-item-stub></todo-item-stub>
</div>
mountのhtml()
it.only("toggleChecked", () => {
const wrapper = mount(TodoContainer, {
data() {
return {
todos: [{ id: 1, text: "text", checked: true }]
}
}
})
console.log(wrapper.html())
})
<div>
<div>
<input class="new">
</div>
<div>
<input type="radio" class="radio">
<span>text</span>
</div>
</div>
結論
mount
の方はコンポーネントすべてをレンダリングしてくれるのですごく便利だったりします。
ただ大規模なコンポーネントになるとstubで表示してくれる shallowMount
の方が便利だったりします。
またshallowMount
+ stub
という書き方もあるので、それも紹介します。
shallowMountのhtml + stubで書く方法
it("shallowMount + stubs", () => {
const wrapper = shallowMount(TodoContainer, {
stubs: {
NewTodo: "<div>NewTodo</div>",
TodoItem: "<div data-test='todo' :id='id' />"
}
})
wrapper.setData({
todos: [
{
id: 1,
text: "text",
checked: false
}
]
})
console.log(wrapper.html())
})
以下のように出力されます。特にコンポーネントをレンダリングする必要ないときは自分は <div>NewTodo</div>
もしくは <div />
でstub を作ったりします。
<div>
<div>NewTodo</div>
<div data-test="todo" id="1"></div>
</div>
また <div data-test="todo" id="1">
みたいに :id="id"
みたいなv-bindもきちんとレンダリングされるのである程度しっかりした検証もできます。
Wrapしない方法
実はwrapしなくてもテストがかけたりします。
import TodoContainer from "@/components/TodoContainer.vue"
it.only("Methodsのテスト", () => {
console.log(TodoContainer.methods)
})
import したコンポーネントから methods
でアクセスできます。
{
addTodo: [Function: addTodo],
toggleChecked: [Function: toggleChecked]
}
メソッドの検証でwrapが必要ないときに使ったりします。
methods: {
addTodo(text) {
this.todos.push({text, checked:false, id: this.todos.length + 1 })
}
}
it.only("Methodsのテスト", () => {
console.log(TodoContainer.methods)
// { addTodo: [Function: addTodo],
// toggleChecked: [Function: toggleChecked] }
const localThis = {
todos: []
}
console.log(localThis)
// { todos: [] }
TodoContainer.methods.addTodo.call(localThis, "test")
console.log(localThis)
// { todos: [ { text: 'test', checked: false, id: 1 } ] }
})
addTodo
を呼び出したときに this
のスコープが変わるので、 call
でテストように this
を渡してあげていますが、それ以外は難しい処理をしていないと思います。
このやり方は結構特殊であまり記事はないので console.log
しまくって あ、これでアクセスできるんだ
的にやっていってほしいのですが もしcreated, mountedで重い処理がある場合、マウントせずにメソッドを呼び出したい時
であれば使えばいいと思います。
他の例はまだ作成中ですが vue-testing-handbook # testing-emitted-events を見ていただけるといいかと思います。
余談
bind系メソッドがhtmlレンダリングされない件
現状Vue-test-utilsのhtml()メソッドで:bind
系の要素が上手くレンダリングされないみたいです。
it.only("toggleChecked", () => {
const wrapper = mount(TodoContainer, {
data() {
return {
todos: [{ id: 1, text: "text", checked: true }]
}
}
})
console.log(wrapper.html())
})
<div>
<div>
<input class="new">
</div>
<div>
<input type="radio" class="radio">
<!--
本当はこっちになってほしい。
<input type="radio" class="radio" checked="true">
-->
<span>text</span>
</div>
</div>
Vue-test-utilsのIssue に記載しておきましたので、そのうち修正されるかもしれません。そのうち。また仮にこのfeatureがマージされると snapshot-test
がたくさん更新されるはずなので、その時はsnapshotだけのPR作らないとね、という話で盛り上がりました。
まとめ
-
mount
は子コンポーネントの html をマウントしてくれる -
shallowMount
は子コンポーネントの html をマウントしない -
shallowMount + stubs
は子コンポーネントの html をstubでマウントできる -
TodoContainer.methods
でメソッドだけのテストは意外に便利
終わりに
いい感じにwrapできるようになりましたか?いい感じにwrapができるようになればVue-test-utilsマスターになれると思いますよ◎