jest.spyOn() で Vue.js コンポーネントからのサービス呼び出しをテストする

Vue.js アプリケーション開発において、コンポーネントから Web API を期待通りに呼んでいるかどうかテストしたかったので、jest.spyOn() を使ってみました
2021.03.31

Web API をコンポーネントから呼んでいる Vue.js アプリケーションがあります。 Web API はサービスクラスとして src/services 以下に実装しています。 ディレクトリのイメージは以下の通りです。

.
├── public
├── src
│   ├── assets
│   ├── components
│   ├── plugins
│   └── services
└── tests
    ├── e2e
    └── unit

コンポーネントから、サービスクラスのパラメータが期待通りに呼ばれているかどうかをテストしたかったので、Jest の jest.spyOn() を使ってみました。

jest.mock() との違い

Jest を使ってモック化する場合、2つの方法があるようです。

前者はモジュール自体(サービスクラス全体)をモック、後者はオブジェクトをモックする、という使い分けをするようです。

今回、サービスクラス全体の挙動を細かくモックで制御したいわけではなく、オブジェクトとして呼び出した単一メソッドをモック化したいだけなので、jest.spyOn() で事足りそうです。

テスト対象

今回テスト用に作成したコンポーネントは以下の通りです。

https://github.com/teknocat/veevalidate-test/blob/master/src/components/WeatherDialog.vue

src/components/WeatherDialog.vue

<template>
  <v-dialog v-model="dialog" scrollable max-width="500px">
    :
    :
    :  
  watch: {
    async dialog(val) {
      if (val) {
        this.loading = true;
        const resWeather = await weatherService.getWeather(35.681147934006624,139.76673203255143);
        this.weather = resWeather.dataseries;
        this.loading = false;
      }
    },
  },
}
</script>

天気予報表示ダイアログ

7Timer! という、緯度、経度から天気情報を返してくれる Web API があったので、それを呼び出して結果をダイアログとして表示するようなコンポーネントとなります。

API 呼び出しを行っているサービスクラスは以下の通りです。

https://github.com/teknocat/veevalidate-test/blob/master/src/services/weatherService.js

src/services/weatherService.js

import axios from 'axios';

export class WeatherService {
  async getWeather(lat, lon) {
    const response = await axios.get(`http://www.7timer.info/bin/api.pl?lon=${lon}&lat=${lat}&product=civillight&output=json`);
    return response.data;
  }
}

export const weatherService = new WeatherService();

引数として緯度、経度を受け取るようなメソッド(getWeather())を定義しています。

テスト

コンポーネントからサービスを正しく呼んでいるかどうかを確認するテストは以下の通りです。

https://github.com/teknocat/veevalidate-test/blob/master/tests/unit/components/WeatherDialog.spec.js

tests/unit/components/WeatherDialog.spec.js

import Vue from 'vue';
import Vuetify from 'vuetify';

import {shallowMount} from '@vue/test-utils'
import WeatherDialog from '@/components/WeatherDialog'
import {WeatherService} from "@/services/weatherService";

Vue.use(Vuetify);

describe('WeatherDialog.vue', () => {
  it('正しくAPIが呼ばれていること', async () => {
    const wrapper = shallowMount(WeatherDialog, {
      propsData: { label: '天気' }
    })

    const spy = jest.spyOn(WeatherService.prototype, 'getWeather').mockReturnValue(Promise.resolve({}));
    wrapper.vm.dialog = true;
    await wrapper.vm.$nextTick();
    expect(spy).toHaveBeenCalled();
    // 引数の数
    expect(spy.mock.calls[0].length).toBe(2);
    // 緯度
    expect(spy.mock.calls[0][0]).toBeGreaterThanOrEqual(0);
    expect(spy.mock.calls[0][0]).toBeLessThanOrEqual(90);
    // 経度
    expect(spy.mock.calls[0][1]).toBeGreaterThanOrEqual(-180);
    expect(spy.mock.calls[0][1]).toBeLessThanOrEqual(180);

    spy.mockClear();
    spy.mockRestore();
  })
})

以下のように記載することで、WeatherServicegetWeather メソッドをモック化することが出来ます。戻り値は特にテストしないので空オブジェクトを返すようにしていますが、必要に応じて期待する戻り値を設定することも可能です。

    const spy = jest.spyOn(WeatherService.prototype, 'getWeather').mockReturnValue(Promise.resolve({}));

jest.mock() でモジュール毎モック化する場合、モジュール内で定義している様々なメソッドやプロパティも何らかのモック実装が必要となりますが、jest.spyOn() はテストしたいメソッドだけ手軽にモック化出来るのが良い点です。

明示的にモックの戻り値を指定しないと実際のオブジェクトが呼ばれる点や、後始末処理を行わないとテスト間で干渉する点など注意が必要な事項もありますが、強力な仕組みだと思います。

おまけ

今回のネタ用に Web API を探していたところ、random.cat という、猫画像をひたすら返す素晴らしい API を発見したので、ついでに組み込みました(テストも書いた)。

猫ダイアログ1

猫ダイアログ2

ソースコード上に cat の文字が踴るのは、心癒されます。

最後に

仕様が明確でないとテストも書けないので、テストが無い=仕様が明確ではない、と言える位にテストを書けるようになりたいです。

例によって、上記確認出来るソースを以下リポジトリに置いています。