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


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

必要なライブラリ

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

jestを使ってモック化するため、cli-plugin-unit-jestを入れました。

最後のflush-promisesはVue Test Utilsの公式サイトで非同期動作のテクニックとして紹介されており、非同期処理を強制的に(?)実行させるためにインストールしました。
非同期のテスト

テストコード

axiosのインポート

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のモック化

jest.mock('axios')

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

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

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

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

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

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

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

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を作成し、それに対して定義してます。

const localVue = createLocalVue()

localVue.prototype.$http = axios

モック関数の他例

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

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

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を使います。

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

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

レスポンスが異なる場合

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

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のドキュメントをご覧ください。

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

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. モジュールを読み込む
import sampleModule from '@/mixin/sampleModule.js'
  1. jest.mockでモック化する
jest.mock('@/mixin/sampleModule')
  1. モック化したいメソッドにjest.fnを入れ込む
sampleModule.methods.sampleMethod = jest.fn((arg1, arg2) => {
  return data
})

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

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

  1. jest.mockでモック化する
const sampleMethod = jest.fn()
  1. mountオプションにmethodsを指定する
const wrapper = mount(sampleComponent, {
  localVue,
  methods: { sampleMethod }
})

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

以上です!