JavaScript
vue.js
jest
vue-test-utils

Vue.jsのテストでコンポーネントをいい感じにwrapする方法

Vue-test-utilsのshallowMountとmountの違いについて

この記事では Vue-test-utils の shallowMountmount の違いについてをメインに記事にしていきます。また 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をフォローしてくれると情報が早いかと思います。

本文

前回のコンポーネントの復習

前回の 記事 を読んでいないかたはぜひ読んできてください。同じコンポーネントを使うことにします。

スクリーンショット 2018-06-24 0.21.35のコピー3.png

$ git clone https://github.com/ykhirao/vue-todos.git
src/components/TodoContainer.vue
<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>

src/components/TodoItem.vue
<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>

src/components/NewTodo.vue
<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()

tests/unit/TodoContainer.spec.js
it.only("toggleChecked", () => {
  const wrapper = shallowMount(TodoContainer, {
    data() {
      return {
        todos: [{ id: 1, text: "text", checked: true }]
      }
    }
  })
  console.log(wrapper.html())
})
console.log.html
<div>
  <new-todo-stub></new-todo-stub>
  <todo-item-stub></todo-item-stub>
</div>

mountのhtml()

tests/unit/TodoContainer.spec.js
it.only("toggleChecked", () => {
  const wrapper = mount(TodoContainer, {
    data() {
      return {
        todos: [{ id: 1, text: "text", checked: true }]
      }
    }
  })
  console.log(wrapper.html())
})
console.log.html
<div>
  <div>
    <input class="new">
  </div>
  <div>
    <input type="radio" class="radio">
    <span>text</span>
  </div>
</div>

https://github.com/vuejs/vue-test-utils/issues/773

結論

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系の要素が上手くレンダリングされないみたいです。

tests/unit/TodoContainer.spec.js
it.only("toggleChecked", () => {
  const wrapper = mount(TodoContainer, {
    data() {
      return {
        todos: [{ id: 1, text: "text", checked: true }]
      }
    }
  })
  console.log(wrapper.html())
})
console.log.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マスターになれると思いますよ◎