axiosInstanceをjestでモックしてテストに使う
はじめに
安全なアプリケーション開発をしてユーザーの不利益になるコードをプロダクションに混ぜないためにもテストの工程は重要です。
今回はJavaScript/TypeScriptのUnitテストのライブラリであるjestを使ってテストを記述します。
通信ライブラリとしてaxiosを使っているので、そのMockの方法とaxiosInstanceを使用したときのテストの書き方も合わせて紹介します。
また、今回はテスト戦略やカバレッジをどう高めていくのかには触れません。
こちらの記事などを参考にしてください。
概要
通信周りはaxiosを使ってaxiosIncetanceを作ります。また、カスタムHooksで作成したaxiosInstanceを使って通信をしています。
このカスタムHooksのためのテストを実行するためにjestでaxiosInstaceをMockしてテストを通すまでの手順を示すのがこの記事の主題です。
使用環境
ts-jest:^29.1.1
axios:^1.6.2
実装方針と詳細について
jestの設定を一通りして実行環境を整え、事前に作ったカスタムHooksを使って実際にテストを実行します。
axiosInstanceとは
axiosにはデフォルトの設定を入れることのできるaxiosInstanceという機能があります。
この機能では
- インターセプターをいれる
- axiosの独自型をサポートしより型安全を高める
- デフォルトの設定であるbaseURLやカスタムヘッダーなどを指定する
などが可能になります。
axiosInstanceの設定の詳細についてはこちらを参照してください。
jestでのモック
jestではいくつかMockを作成する方法があります。
以下ではいくつかsampleを出しながらMockをする方法を紹介します。ちなみに今回はjest.mock()を使うことを選択しました。
自動的に読み込んだモジュールをすべてMockしてくれるので自分たちの仕様を反映したaxiosInstanceのチェックも合わせて行いたいためこちらを採用しています。
jest.fn()を使う方法
jest.fn()を使うと、新しいモック関数を作成できます。作成したモック関数には、呼び出し回数や引数を記録するmockプロパティが付与されます。
const mockFn = jest.fn();
mockFn(1, 2); // モック関数を呼び出す
expect(mockFn).toHaveBeenCalledWith(1, 2); // 引数のテスト
jest.spyOn()を使う方法
jest.spyOn(object, methodName)を使うと、既存のオブジェクトのメソッドをモック化できます。元のメソッドの実装を上書きし、モック関数として振る舞うようになります。
const video = {
play: () => { /* 実装 */ },
stop: () => { /* 実装 */ }
};
const spy = jest.spyOn(video, 'play');
video.play(); // モック化されたplay()が呼び出される
jest.mock()を使う方法
jest.mock(modulePath)を使うと、指定したモジュール全体をモック化できます。モック化されたモジュールは、デフォルトで空のモック関数になります。
jest.mock('axios'); // axiosモジュール全体をモック化
const axios = require('axios');
axios.get = jest.fn(() => Promise.resolve({ data: {} })); // モック関数の振る舞いを定義
axiosInstanceをモック
axiosInstanceを自作してみます。
// axiosインスタンスを作成
const axiosInstance = axios.create({
baseURL: 'https://api.example.com'
});
// リクエストインターセプターを追加
axiosInstance.interceptors.request.use(config => {
// 認証トークンを追加する等の処理
return config;
});
// データ取得
const response = await axiosInstance.get('/users');
このテストを書くためには
- テストファイル内でaxiosInstanceをインポートする
- axiosInstanceをMockする
- テストを実行
具体的には以下のようなコードを書きます
const axios = require('axios');
jest.mock('axiosInstance'); //自作したaxiosInstanceをMock
describe('fetchData', () => {
let fetchData;
beforeEach(() => {
// モックをリセット
axios.get.mockClear();
// テスト対象の関数をモック化
fetchData = jest.fn(() => axios.get('/api/data'));
});
afterEach(() => {
jest.resetAllMocks();
});
it('should fetch data successfully', async () => {
// モック応答を設定
const mockResponse = { data: { message: 'Success' } };
axios.get.mockResolvedValueOnce(mockResponse);
// テスト対象の関数を実行
await fetchData();
// 期待する動作を検証
expect(axios.get).toHaveBeenCalledWith('/api/data');
expect(fetchData).toHaveReturned();
});
it('should handle error', async () => {
// モックエラーを設定
const mockError = new Error('Network Error');
axios.get.mockRejectedValueOnce(mockError);
// テスト対象の関数を実行
await fetchData();
// 期待する動作を検証
expect(axios.get).toHaveBeenCalledWith('/api/data');
expect(fetchData).toHaveReturned();
// エラーハンドリングのテストを追加
});
});
テストの実行
jestのコマンドに従ってテストを実行してください。
細かい実行時オプションは公式を参考にしてください。
高度なモック
特定の条件下で異なる応答を返すようにモックを設定できます。jest.fn()で作成したモック関数には、mockImplementationOnce()やmockReturnValueOnce()などのメソッドがあり、それらを使って条件付きの応答を定義できます。
const mockFn = jest.fn();
// 最初の呼び出しでは'first'を返す
mockFn.mockReturnValueOnce('first');
// 2回目の呼び出しでは'second'を返す
mockFn.mockReturnValueOnce('second');
// 3回目以降は'other'を返す
mockFn.mockReturnValue('other');
mockImplementation()を使えば、より詳細なエラーケースをモック化できます。例えば、特定の引数が渡された場合にエラーを投げるようにモックできます。
const mockFn = jest.fn();
// 引数が'error'の場合にエラーを投げる
mockFn.mockImplementation(arg => {
if (arg === 'error') {
throw new Error('Invalid argument');
}
// 正常系の処理
});
躓いたこと
ESMがjestデフォルトの設定だとサポートされていない。
これはjestがESMへの対応の過渡期にあるためです。もともとcjsなどにまとめられた実行ファイルを実行していたため別途この設定が必要になります。なのでNext.jsなどを使っている場合にはテストファイルが実行される環境について別途設定を書き加える必要がありました。
jest.config.jsとtsconfigに追記が必要です。
ESMをjestで実行するための詳細については公式を参照してください。
まとめ
axiosを使ってaxiosInstanceを生成しテストを通すまでの流れをおさらいしました。
テストはモダンフロントエンドに置いては重要な立ち位置の一つです。
より安全な開発、より安全なデリバリーを実現しクオリティを担保するために必須と言ってよいでしょう。
また今回はaxiosInstaceのMockがうまくいかなかったり、ESMを回避する設定が無いせいでモジュール自体がそもそも読み込まれないなどがありました。
このあたりもフロントエンドの進化とともに変わっていくことなので引き続き動向を見ながらより低コストで可読性の高いテストで開発を下支えしていきたいですね。
Discussion