vue-test-utilsとjestでLifecycle Hookのメソッドをモックする

本記事ではvue-test-utilsとjestでvue.jsのテストを書くときにLifecycle Hook（mountedなど）のメソッドの処理をモックする方法を紹介します。
結論から言うと、mountedフック自体をモックするのではなく、mountedフック内の処理をメソッド化してそのメソッドをモックすることで対応します。

## 動作環境
主なフレームワーク/ライブラリのバージョンです。

| フレームワーク/ライブラリ |       バージョン |
|:-----------------|------------------:|
| vue             |            2.6.10 |
| vue-test-utils  |     1.0.0-beta.29 |
| jest            |            24.5.0 |


## やりたいこと
componentの単体テストを行う場合にmountedの処理をモックしたい。
以下のような場合を想定。

- mountedの中で複雑な処理やapiを何度も呼ぶような準備コストが大きい
- vueのコンポーネントの外にあるhtml内のjavascriptの変数を読み込んで初期化しているような場合


## サンプル
### コンポーネント
例として以下のCounter.vueのmountedの処理をmockすることを考えます。

```vue:Counter.vue
<template>
  <div>
    <h1>Click and Count</h1>
    <button v-on:click="increment">+1</button>
  </div>
</template>

<script>
export default {
  name: 'Counter',
  data() {
    return {
      count: 0,
    };
  },
  mounted() {
    // 例） このコンポーネントの外にあるhtmlに直接埋め込まれた
    //    $initialCountという変数の値をcountに設定して初期化する
    this.count = $initialCount;
  },
  methods: {
    increment() {
      this.count += 1;
    }
  },
};
</script>
```

### コンポーネントのテスト
以下のようにbuttonをclickするとカウントアップされることをテスト。

```js
import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';

describe('Counter.vue', () => {
  it('click button and countup', () => {
    const wrapper = shallowMount(Counter);
    const beforeCount = wrapper.vm.count;
    wrapper.find('button').trigger('click');

    expect(wrapper.vm.count).toBe(beforeCount + 1);
  });
});
```

今のままだと$initialCountの変数が定義されていないのでテストしたい処理にたどり着く前に失敗してしまう。
<img width="640" alt="スクリーンショット 2019-03-30 20.19.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/132008/d0ee3376-2432-3f8a-df3c-b7bd95cb0089.png">

## 初期化処理をメソッド化してモックする
外部から取得する変数自体をメソッド化してしまえばモックできるので、メッソッド化します。

### 修正後のコンポーネント

```vue:Counter.vue
<template>
  <div>
    <h1>Click and Count</h1>
    <button v-on:click="increment">+1</button>
  </div>
</template>

<script>
export default {
  name: 'Counter',
  data() {
    return {
      count: 0,
    };
  },
  mounted() {
    // 例） このコンポーネントの外にあるhtmlに直接埋め込まれた
    //    $initialCountという変数の値をcountに設定して初期化する
-   // this.msg = $initialCount;
+    this.count = this.initialCount();
  },
  methods: {
+   initialCount() {
+     return $initialCount;
+   },
    increment() {
      this.count += 1;
    }
  },
};
</script>
```

### 修正後のテスト

```js
import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';

describe('Counter.vue', () => {
  it('click button and countup', () => {
    const count = 10;
    // initialCount()をmock
    const initialCount = jest.fn();
    initialCount.mockReturnValue(count);
    // methodsとして渡すことでinitialCountが呼ばれるとmockに設定した戻り値が返る
    const wrapper = shallowMount(Counter, { methods: { initialCount } });
    // クリックしてカウントされることを確認
    wrapper.find('button').trigger('click');
    expect(wrapper.vm.count).toBe(count + 1);
  });
});
```

### 結果
無事にテストが通りました。

<img width="513" alt="スクリーンショット 2019-03-30 20.49.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/132008/f5d680ef-4aeb-ba42-10c4-9601eb43ba81.png">

## まとめ
mountedやcreatedなどのライフサイクルフック内の処理が準備できないような場合（そもそものつくりが悪いというのもあるかもしれませんが）は、処理をメソッド化することで回避する方法を紹介しました。
もっとスマートなやり方があるよなどあればばコメントいただけると幸いです。

## 余談

最初はmounted自体を上書けると思い、以下のようなコードで試しましてみましたが元のmountedの処理が動作してしまいテストに失敗しました。

```js
import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';

describe('Counter.vue', () => {
  it('click button and countup', () => {
    const count = 0;
    const mounted = jest.fn();
    // mountedにjestのmock関数を置く
    const wrapper = shallowMount(Counter, { mounted });
    wrapper.find('button').trigger('click');
    expect(wrapper.vm.count).toBe(count + 1);
  });
});
```

少しソースを修正してもう少し中の動きを身を見てみると

```vue:Counter.vue
<template>
// ・・・
</template>

<script>
export default {
  name: 'Counter',
  data() {
    // ・・・
  },
  mounted() {
    // this.count = $initialCount;
    // 動作を確認するためのログ
    console.log('original mounted');
  },
  methods: {
    // ・・・
  },
};
</script>
```

もともとのmountedとmountedオプションを設定した二つのwrapperでログを確認してみる

```js
import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue';

describe('Counter.vue', () => {
  it('click button and countup', () => {
    const mounted = () => {
      // 動作を確認するためのログ
      console.log('mock mounted');
    };

    // mountedオプションを設定しない
    const wrapper1 = shallowMount(Counter);
    console.log(wrapper1.vm.$options.mounted);

    // mountedオプションを設定する
    const wrapper2 = shallowMount(Counter, { mounted });
    console.log(wrapper2.vm.$options.mounted);
  });
});
```

mountedを追加したwrapper2　のほうは、`wrapper2.vm.$options.mounted`で二つの関数が返ってきています。`shallowMount`時に追加した関数で上書くのではなく追加され、`mouted()`内のログから`original`と`mock`両方が動いているようでした。

<img width="653" alt="スクリーンショット 2019-03-31 11.52.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/132008/4af6500e-6bd5-2898-2a3b-08d82fd2e076.png">


少し古いやりとりですが、
https://github.com/vuejs/vue-test-utils/issues/148
でライフサイクルフックのモックについてのやりとりがされていて、vue.jsコアチームの[Edd Yerburgh](https://github.com/eddyerburgh)
いわく
> I think we should add support for mocking all lifecycle events, or none.

とあるので追加されてこの辺りも改善されることを期待です。