【Vue】Jestを使った Vue / TypeScript の単体試験 ~ axios / あれこれ 編 ~

■ はじめに

https://dk521123.hatenablog.com/entry/2021/01/07/230317
https://dk521123.hatenablog.com/entry/2021/01/27/225148
https://dk521123.hatenablog.com/entry/2021/03/13/000000
https://dk521123.hatenablog.com/entry/2021/03/23/231011

の続き。

今回は、前回行った axios の モック化をもう少し掘り下げる。

結構、Github の README が充実していて、分かりやすい

https://github.com/ctimmerm/axios-mock-adapter

目次

【1】モックのURLが合致しなかった場合の対処 - onNoMatch
 1)onNoMatch: 'throwException'
 2)その他の方法
【2】複数のAPIに対応するには
 1)正規表現を使う
 2)onAnyを使う
 補足:複数のMockを纏めて定義する
【3】ネットワークエラー/タイムアウトの試験を行うには
【4】後処理

【1】モックのURLが合致しなかった場合の対処 - onNoMatch

mockAxios.onGet('https://xxxx/users').reply(...
と指定した場合、「https://xxxx/users」以外がきた場合の対処。
 => MockAdapterインスタンス作成時に指定できる

1)onNoMatch: 'throwException'

// 一致しなかった場合、例外
mockAxios = new MockAdapter(axios, { onNoMatch: 'throwException' });

補足 - onNoMatch: 'passthrough'

// 一致しなかった場合、無視
// Mock all requests to /foo with HTTP 200, but forward
// any others requests to server
mockAxios = new MockAdapter(axios, { onNoMatch: 'passthrough' });

2)その他の方法

「【2】複数のAPIに対応するには」の「B) onAnyを使う」で記載。
 => サンプルの場合、「else」で500番を返すようにしている

【2】複数のAPIに対応するには

1)正規表現を使う

その1

mock.onGet(/\/users\/\d+/).reply(function (config) {

その2

const usersUri = "/users";
const url = new RegExp(`${usersUri}/*`);

mock.onGet(url).reply(200, users);

2)onAnyを使う

// Expected order of requests:
const responses = [
  ["GET", "/foo", 200, { foo: "bar" }],
  ["POST", "/bar", 200],
  ["PUT", "/baz", 200],
];

// Match ALL requests
mock.onAny().reply((config) => {
  const [method, url, ...response] = responses.shift();
  if (config.url === url && config.method.toUpperCase() === method)
    return response;
  // Unexpected request, error out
  return [500, {}];
});

補足:複数のMockを纏めて定義する

const initializeMocks = (additionalMocks = {}) => {
  const mocks = {
    mock1: () => {
      mockAdapter.onGet('xxxx').reply((config) => {
         return [200, { id: 'x-001'}];
      });
    },
    mock2: () => {
      mockAdapter.onGet('yyyy').reply((config) => {
         return [200, { id: 'x-002'}];
      });
    },
    ...additionalMocks,
  };

  Object.values(mocks).forEach(mock => {
    mock();
  });
  return mocks;
}

サンプル

* 以下の関連記事のサンプルを上記の書き方で書き直してみる

https://dk521123.hatenablog.com/entry/2021/03/23/231011

import { shallowMount, Wrapper } from '@vue/test-utils';
import Demo2 from '@/views/Demo2.vue';

import flushPromises from 'flush-promises';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

describe('Demo2.vue', () => {
  let wrapper: Wrapper<Demo2>;
  let mockAxios: MockAdapter;
  console.info = jest.fn();
  console.error = jest.fn();

  const initializeMocks = (overrideMocks = {}) => {
    const mocks = {
      todosMock: () => {
        const regExp = new RegExp('.*/todos/(.+)');
        mockAxios.onGet(regExp).reply((config) => {
          let mockData = {};
          const result = config.url?.match(regExp);
          if (result) {
            mockData = {
              userId: 1,
              id: result[1],
              title: 'sample',
              completed: false,
            };
          }
          return [200, mockData];
        });
      },
      usersMock: () => {
        mockAxios.onGet(new RegExp('.*/user')).reply(() => [200, [
          {
            id: 1,
            name: 'Mike',
            email: 'mike@gmail.com',
          },
          {
            id: 2,
            name: 'Tom',
            email: 'tom@gmail.com',
          },
          {
            id: 3,
            name: 'Sam',
            email: 'sam@gmail.com',
          },
        ]]);
      },
      ...overrideMocks,
    };

    Object.values(mocks).forEach((mock) => {
      mock();
    });
    return mocks;
  };

  beforeEach(() => {
    mockAxios = new MockAdapter(axios, { onNoMatch: 'throwException' });
  });

  it('This is just a demo test', async () => {
    initializeMocks();
    wrapper = shallowMount(Demo2);

    await flushPromises();

    const vm = (wrapper.vm as any);
    console.log(vm.items);
    expect(vm.items[0].id).toEqual(1);
    expect(vm.items[0].name).toEqual('Mike');
    expect(vm.items[0].email).toEqual('mike@gmail.com');
    expect(vm.items[1].id).toEqual(2);
    expect(vm.items[1].name).toEqual('Tom');
    expect(vm.items[1].email).toEqual('tom@gmail.com');
    expect(vm.items[2].id).toEqual(3);
    expect(vm.items[2].name).toEqual('Sam');
    expect(vm.items[2].email).toEqual('sam@gmail.com');

    expect(console.info).toHaveBeenCalledWith('Response OK');
  });

  it('This is a network error test', async () => {
    const usersErrorMock = {
      usersMock: () => {
        mockAxios.onGet(new RegExp('.*/user')).networkError();
      }
    };
    initializeMocks(usersErrorMock);
    wrapper = shallowMount(Demo2);

    await flushPromises();

    expect(console.error).toHaveBeenCalledWith('Error...');
  });
});

4)ネットワークエラー/タイムアウトの試験を行うには

https://github.com/ctimmerm/axios-mock-adapter

より抜粋

Mock a low level network error

// Returns a failed promise with Error('Network Error');
mock.onGet("/users").networkError();

// networkErrorOnce can be used to mock a network error only once
mock.onGet("/users").networkErrorOnce();

Mock a network timeout

// Returns a failed promise with Error with code set to 'ECONNABORTED'
mock.onGet("/users").timeout();

// timeoutOnce can be used to mock a timeout only once
mock.onGet("/users").timeoutOnce();

5)後処理

https://github.com/ctimmerm/axios-mock-adapter

mockAdapter.reset();
もしくは
mockAdapter.restore()
でモックのリセットができる。

reset / restore との違い

reset is different from restore in 
that restore removes the mocking from the axios instance completely,
 => restore() は完全にaxios インスタンスからモックを削除する

whereas reset only removes all mock handlers that were added with onGet, onPost, etc. 
but leaves the mocking in place.
 => 一方、resetは、onGet, onPostなどに追加したモックハンドラ
  の全てを削除するが、モックはそのままにする。

関連記事

Jestを使った Vue / TypeScript の単体試験 ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2021/01/07/230317
Jestを使った Vue / TypeScript の単体試験 ~ 基本編 ~
https://dk521123.hatenablog.com/entry/2021/01/27/225148
Jestを使った Vue / TypeScript の単体試験 ~ 非同期編 ~
https://dk521123.hatenablog.com/entry/2021/03/13/000000
Jestを使った Vue / TypeScript の単体試験 ~ axios 編 ~
https://dk521123.hatenablog.com/entry/2021/03/23/231011
非同期処理 ~ async/await, Promise ~
https://dk521123.hatenablog.com/entry/2021/01/16/202822
axios ~ 外部API通信ライブラリ ~
https://dk521123.hatenablog.com/entry/2021/01/14/101645
JS単体試験ツール Jest ~ Mock編 ~
https://dk521123.hatenablog.com/entry/2021/01/26/215558