Vitestでfetchのmockを作成し、Fixtureとして分離する
始めに
Vitest で API リクエストのテストコードを作成する際に、
fetch 関数の mock の作成や、それらを beforeAll
やbeforeEach
などを用いて、
Fixture として分離する方法を調べたのでまとめてみます。
API リクエストのテストをしたいが、実際の API にはリクエストできない場合、
Vitest で fetch 関数を偽装して指定したレスポンスを返すといったやり方があると思います。
describe('get', () => {
it('APIリクエスト成功', async () => {
//fetchが指定したレスポンスを返すように定義する
const fetchMock = vi.spyOn(global, 'fetch');
fetchMock.mockImplementation(async () => json(data, { status: 200 }));
//リクエスト処理
//expectでの判定処理
});
});
また、fetch のmockImplementation()
では、
コールバック関数の引数でinput: string | URL | Request
を受け取って使用できます。
テストケースが複数ある場合には都度書くのは面倒なので、
beforeAll
やbeforeEach
などを使って各テストの前に定義できます。
describe('getByType', () => {
const fetchMock = vi.spyOn(global, 'fetch');
//it実行前に1度だけfetchの動きを定義
beforeAll(() => {
fetchMock.mockImplementation(async (input) => {
//inputからリクエストURLが分かるので、指定したデータを返す
const dataMap: { [key: string]: TestData[] } = {
[`${requestUrl}?type=A`]: [testData[0]],
[`${requestUrl}?type=B`]: [testData[1]],
[`${requestUrl}?type=C`]: [testData[2]]
};
return dataMap[input as string]
? json(dataMap[input as string], { status: 200 })
: new Response('データが見つかりません', { status: 400 });
});
});
it('type=AでのAPIリクエスト成功', async () => {
//リクエスト処理
//expectでの判定処理
});
it('type=BでのAPIリクエスト成功', async () => {
//リクエスト処理
//expectでの判定処理
});
it('type=CでのAPIリクエスト成功', async () => {
//リクエスト処理
//expectでの判定処理
});
});
パラメータで分岐する処理のテストに有用ですが、
他のテストでも同様の mock を作る...となると毎回書くのは面倒です。
Fixture として分離する
単に成功したレスポンスを返すだけなら毎回書いても良いかなと思いますが、
分岐処理が入る場合は、Fixture として分離しておいて再利用できるようにするのが良さそうです。
Vitest のbeforeAll
などは describe 内で直接実行するだけでなく、
外部に定義してから describe 内で呼び出すことでも、
同様に各タイミングで設定した hooks を実行することが可能です。
(恐らく describe 内ではbeforeAll
やbeforeEach
などの hooks が最初から定義されているが、
実行される中身のコールバック関数は空になっており、
コールバック関数を設定した hooks を実行することで、実行される処理を予約(設定)できるという動き?)
今回は単に成功したレスポンスを返すものを含めて、以下のように定義して呼び出してみました。
(実際には別ファイルに定義して、各テストで呼び出します)
export const setGetAPIFetchMock = () => {
const fetchMock = vi.spyOn(global, 'fetch');
fetchMock.mockImplementation(async () => json(getData, { status: 200 }));
}
export const setGetByTypeAPIFetchMock = () => {
const fetchMock = vi.spyOn(global, 'fetch');
beforeAll(() => {
fetchMock.mockImplementation(async (input) => {
const dataMap: { [key: string]: TestData[] } = {
[`${requestUrl}?type=A`]: [testData[0]],
[`${requestUrl}?type=B`]: [testData[1]],
[`${requestUrl}?type=C`]: [testData[2]]
};
return dataMap[input as string]
? json(dataMap[input as string], { status: 200 })
: new Response('データが見つかりません', { status: 400 });
});
});
};
describe('get', () => {
it('APIリクエスト成功', async () => {
//Fixtureを呼ぶ
//別のテストケースでも同じレスポンスを使いたいなら、it前に実行でも可
setGetAPIFetchMock();
});
});
describe('getByType', () => {
//beforeAll()を実行するFixtureをitの前に呼ぶ
setGetByTypeAPIFetchMock();
it(() => {
//itでテストケースを定義
});
});
もうちょっとわかりやすく
若干脱線しますが、他にも関数を作った時を考えると Fixture の名前をちょっと調整したいです。
インポート方法を少し変えて、クラスのメソッドを呼ぶ時のようにしてみます。
//Fixture関数の名前を変更。
export const setGetFetch = () => {
//省略
}
export const setGetByTypeFetch = () => {
//省略
};
import * as APIMock from './APIMock';
//APIMockという名前空間でエクスポート
export { APIMock };
//Fixtureをまとめてインポート
import { APIMock } from './index';
describe('get', () => {
it('APIリクエスト成功', async () => {
//Fixtureを呼ぶ
APIMock.setGetFetch();
});
});
describe('getByType', () => {
//Fixtureを呼ぶ
APIMock.setGetByTypeFetch();
it(() => {
});
});
Fixture の種類が複数あり、ファイルを分けた場合は index.ts に記載して、
スコープを分けてインポートできるようになれば分かりやすく運用できそうです。
誤りや指摘等あればコメントお願いします。
Discussion