Vue.jsでaxiosをモック化してテストする！

axiosのモック化にかなり手間取ったため、作業メモとして残しておこうと思いました。

## 必要なライブラリ

* @vue/cli-plugin-unit-jest
* @vue/test-utils
* flush-promises

jestを使ってモック化するため、`cli-plugin-unit-jest`を入れました。  
最後の`flush-promises`はVue Test Utilsの公式サイトで非同期動作のテクニックとして紹介されており、非同期処理を強制的に（？）実行させるためにインストールしました。
[非同期のテスト](https://vue-test-utils.vuejs.org/ja/guides/testing-async-components.html)

## テストコード

### axiosのインポート

``` js
import axios from 'axios'
```

まずはaxiosモジュールをインポートします。  
システムによっては、axiosの設定を`index.js`に書いていると思いますが、テストコードではこの`index.js`ではなく、生（と言っていいかは謎ですが）のaxiosをimportします。
理由としては、`index.js`をimportすると、そのファイルに書かれている、例えばaxios.create等のメソッドもモック化しないといけないからです。
もちろん、別で`index.js`のテストは必要なのですが、今回のテスト対象は、axios.postやgetを使っているコンポーネントであるため、`index.js`ではなく、`axios`をimportします。

### axiosのモック化

``` js
jest.mock('axios')
```

axiosモジュールをモック化します。
これで、プロダクトコードでaxiosを呼び出した場合、これから登録するモック関数が呼ばれるようになります。
この`jest.mock('axios')`は、describeの前に実行してください。

### mountまたはshallowMountの実行時オプションにsync: falseを付ける

``` js
const wrapper = mount(sampleComponent, { sync: false })
```

### axios.postにモック関数を登録する

例えばこのようなプロダクトコードがあるとします。

``` js
methods: {
  aFunc() {
    axios.post('/sample')
      .then(response => {
        console.log('成功')
      })
      .catch(error => {
        console.log('失敗')
      })
  }
}
```

このaxios.postから返すレスポンスをモックにします。  

``` js
const response = {
  message: '成功'
}
axios.post.mockImplementationOnce((url) => {
  return Promise.resolve(response)
})
```

`mockImplementationOnce`は、`post`が呼び出されたときに「一度だけ」関数の中が実行されます。  
もし、テスト対象のプロダクトコードでpostが2回呼び出されていた時には、1回目のpostは上記で設定した`Promise.resolve`が返りますが、2回目のpostはエラーになります。
この場合の解決策としては2つあります。それについては後述します。
ちなみにプロダクトコードで`this.$http`のように使われている場合は、テストコードのimport文直後くらいに`prototype.$http = axios`を入れてください。
私はcreateLocalVueでlocalVueを作成し、それに対して定義してます。

``` js
const localVue = createLocalVue()

localVue.prototype.$http = axios
```

#### モック関数の他例

> もし、テスト対象のプロダクトコードでpostが2回呼び出されていた時には、1回目のpostは上記で設定した`Promise.resolve`が返りますが、2回目のpostはエラーになります。

例えばこのようなコードがあった場合は、`mockImplementationOnce`を一つ指定しただけだとエラーになります。

``` js
methods: {
  aFunc() {
    axios.post('/sample')               // post呼び出し1回目
      .then(response => {
        console.log('/sample成功')
        axios.post('/sample2')          // post呼び出し2回目
          .then(response => {
            console.log('/sample2成功')
          })
          .catch(error => {
            console.log('/sample2失敗')
          })
      })
      .catch(error => {
        console.log('/sample失敗')
      })
  }
}
```

ではどうすればよいか、ですが、求めるレスポンスによって2通りのやり方があります。

##### レスポンスが同じでも良い場合

`jest.fn().mockImplementation`を使います。  

``` js
const response = {
  message: '成功'
}
axios.post.mockImplementation((url) => {
  return Promise.resolve(response)
})
```

こうすると`axios.post`が呼ばれたときには **常に**responseが返されます。  

##### レスポンスが異なる場合

`jest.fn().mockImplementationOnce`を呼び出されるAPIの数分定義します。  

``` js
const response1 = {
  message: '成功'
}
const response2 = {
  message: '失敗'
}
axios.post.mockImplementationOnce((url) => {
  return Promise.resolve(response1)
}).mockImplementationOnce((url) => {
  return Promise.resolve(response2)
})
```

こうすることで、1回目のAPI呼び出しのときはresponse1が返り、2回目のAPI呼び出しの時はresponse2が返ります。

### モックの検証

きちんと想定通りのモックが呼ばれているかを検証します。
検証したいことによって、いろいろとメソッドが用意されていますが、ここでは`toHaveBeenCalledWith`を使います。
その他のメソッドについては[Jestのドキュメント](https://jestjs.io/docs/ja/expect)をご覧ください。

**レスポンスが異なる場合**の検証をしてみましょう。

``` js
expect(axios.post).toHaveBeenCalledWith('/sample')
expect(axios.post).toHaveBeenCalledWith('/sample2')
```

`expect`の引数には、実行したモックを指定します。
`toHaveBeenCalledWith`の引数には、APIが実行されたときの引数を指定します。
今回、API実行時にはurlのみを引数としてaxios.postに渡しているため、それを指定します。
params等の引数を第2引数に渡している場合は、`expect(axios.post).toHaveBeenCalledWith('/sample', { param1: 'aaa' })`のように渡します。

## axiosではなく作ったモジュールの一部をモック化する！

1. モジュールを読み込む

``` js
import sampleModule from '@/mixin/sampleModule.js'
```

2. `jest.mock`でモック化する

``` js
jest.mock('@/mixin/sampleModule')
```

3. モック化したいメソッドに`jest.fn`を入れ込む

``` js
sampleModule.methods.sampleMethod = jest.fn((arg1, arg2) => {
  return data
})
```

これでモック化できます。

## テスト対象コンポーネントのメソッドをモック化する！

1. `jest.mock`でモック化する

``` js
const sampleMethod = jest.fn()
```

2. mountオプションに`methods`を指定する

``` js
const wrapper = mount(sampleComponent, {
  localVue,
  methods: { sampleMethod }
})
```

ちゃんと調べたわけではないのですが、どうやらconstの名前のメソッドがモック化されるようです。

以上です！