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

# 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://qiita.com/re-fort/items/63bef6778bf3d3939815)

ドキュメントは https://vue-test-utils.vuejs.org/ja/ こちらになります。今回の内容は、ドキュメントに書いてあることがほとんどなので、私の記事からテストに入った方もぜひドキュメントを読んでみてください。このドキュメントは長くないので流し読みなら1時間もあれば読めると思います。

## はじめに

こんにちは。僕です。

前回は [【Vue.js】いつから「フロントエンド開発でTDDができない」と錯覚していた？](https://qiita.com/ykhirao/items/becd9be857dbe6804314) ※1 という記事を書かせていただきました。。フロントエンド界隈ってまったくテストが盛り上がっていなくて、ましてや「弊社TDDやってます」なんという声とかはほとんどあがっていない状況です。（私はそう感じているという主観的な意見ですが。）

※1 タイトルの元ネタ BLEACHの藍染惣右介のセリフより

「フロントエンドのテストって難しくないよ！！」とか「フロントエンドのロジック部分はTDDできるよ！！」って伝えたくて記事を書かせていただきました。基本的にVueのテストについての記事はQiitaに書いていくので、QiitaかTwitterをフォローしてくれると情報が早いかと思います。

## 本文

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

前回の [記事](https://qiita.com/ykhirao/items/becd9be857dbe6804314) を読んでいないかたはぜひ読んできてください。同じコンポーネントを使うことにします。

<img width="643" alt="スクリーンショット 2018-06-24 0.21.35のコピー3.png" src="https://qiita-image-store.s3.amazonaws.com/0/112929/ac256146-7880-c85f-82d9-4d7e80711a0d.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で書く方法

```js
  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 を作ったりします。

```html
<div>
  <div>NewTodo</div>
  <div data-test="todo" id="1"></div>
</div>
```

また `<div data-test="todo" id="1">` みたいに `:id="id"` みたいなv-bindもきちんとレンダリングされるのである程度しっかりした検証もできます。

### Wrapしない方法

実はwrapしなくてもテストがかけたりします。

```js
  import TodoContainer from "@/components/TodoContainer.vue"

  it.only("Methodsのテスト", () => {
    console.log(TodoContainer.methods)
  })
```

import したコンポーネントから `methods` でアクセスできます。

```html
{
  addTodo: [Function: addTodo],
  toggleChecked: [Function: toggleChecked]
}
```

メソッドの検証でwrapが必要ないときに使ったりします。

```js
  methods: {
    addTodo(text) {
      this.todos.push({text, checked:false, id: this.todos.length + 1 })
    }
  }
```

```js
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](https://lmiller1990.github.io/vue-testing-handbook/ja/testing-emitted-events.html) を見ていただけるといいかと思います。

### 余談
#### 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](https://github.com/vuejs/vue-test-utils/issues/773) に記載しておきましたので、そのうち修正されるかもしれません。そのうち。また仮にこのfeatureがマージされると `snapshot-test` がたくさん更新されるはずなので、その時はsnapshotだけのPR作らないとね、という話で盛り上がりました。

## まとめ

- `mount` は子コンポーネントの html をマウントしてくれる
- `shallowMount` は子コンポーネントの html をマウントしない
- `shallowMount + stubs` は子コンポーネントの html をstubでマウントできる
- `TodoContainer.methods` でメソッドだけのテストは意外に便利

## 終わりに

いい感じにwrapできるようになりましたか？いい感じにwrapができるようになればVue-test-utilsマスターになれると思いますよ◎