はじめまして、インターンのmasuyamaと申します。rexcornuではフロントエンド開発を担当しています。
今まで業務経験がなかったため、こうして単体テストのコードを書くのは初めての挑戦です。
今回は、単体テストを書く上で学んだことを共有するために、主にJestのMockと呼ばれる機能に焦点を当てて記事を書いてみたいと思います。
(〜本ノックと題していますがクイズ系の記事ではありません)
はじめに:
そもそも「Jestとは?」「Mockとは?」と思う方もいるはずなので、簡単に用語および開発背景の説明を行います。
Jestとは?
JestはMeta社が開発したJavaScriptテスティングフレームワークです。
Mockとは?
Mockとは、テスト対象のコードが依存する外部のシステムやオブジェクトを模倣するための機能です。
例えば、データベースや外部APIなど、実際にはテスト実行時にアクセスしたくない外部依存性がある場合に、これらを模擬的に再現したオブジェクトを使用します。
つまり、Mock化の主な目的は、テスト対象が持つ外部依存性を排除することにあります。
単体テストの目的
単体テストは、プログラムの個々のモジュール(関数、メソッド、コンポーネント)が正しく動作するかどうかを検証するためのテストです。
テスト対象となるモジュールが他のモジュールや外部システムに依存している場合、その依存関係を排除してテストを行う必要があります。
今回の記事では、JestのMock機能を使って、テスト対象のコードが依存する外部のシステムやオブジェクトを模倣する方法をいくつか紹介します。
Mockパターン
それでは、僕が実際に書いたMock化のコードを、より一般的な例に移し替えたうえで、4つほど紹介していきたいと思います。
1. window.ResizeObserverのMock化
window.ResizeObserverとは、DOM要素のサイズ変更を監視するためのAPIです
今回はブラウザでの環境を想定していないため、window.ResizeObserverをそのまま使うことはできません。
そのため、window.ResizeObserverをMock化して、テスト実行時にエラーが発生しないようにする必要があります。
下のテストコードではResizeObserverのコンストラクタをjest.fn()でMock化し、空のobserveメソッドとdisconnectメソッドを持つオブジェクトを返すようにしています。
テストコード
window.ResizeObserver = jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
}));
2. axiosのget通信のMock化
例えば、以下のようなaxiosのget通信があるとします。
このコードをテストするためには、axiosのget通信をMock化する必要があります。
あらかじめget通信のレスポンスのダミーをjsonファイルとして用意しておき、axios.getの戻り値をPromise.resolveで返すようにします。
テスト対象のコード
import axios from 'axios';
axios.get("user_list").then((res) => {
console.log(res.data);
}).catch((err) => {
console.log(err);
});
axios.get("product_list").then((res) => {
console.log(res.data);
}).catch((err) => {
console.log(err);
});
テストコード
const dummyUserList = require(./mock/dummyUserList.json)
const dummyProductList = require(./mock/dummyProductList.json)
jest.mock("./axios");
axios.get = jest.fn();
axios.get.mockImplementation((url) => {
if (url === "user_list") {
return Promise.resolve({ data: dummyUserList });
}
if (url === "product_list") {
return Promise.resolve({ data: dummyProductList });
}
return Promise.resolve({data:{}})
});
3. localStorageのMock化
下のようにlocalStorageを使ってデータを保存している場合、localStorageをMock化してテストを行う必要があります。
localStorage.getItemメソッドのMock化には、jest.spyOnを使用します。
jest.spyOnは、オブジェクトのメソッドをMock化するための関数です。
localStorage.getItemメソッドをMock化することで、テスト実行時にlocalStorage.getItemメソッドが呼び出された場合に、指定した値を返すようにします。
テスト対象のコード
const basket = localStorage.getItem('basket');
const user = localStorage.getItem('user');
テストコード
const dummyBasket = require(./mock/dummyBasket.json)
jest.spyOn(Storage.prototype, "getItem").mockImplementation((key) => {
if (key === 'basket') {
return dummyBasket;
}
if(key == 'user') {
return “testUser”
}
return 0;
});
4. useContextのmock化
外部のContextを扱うコンポーネントをテストする場合、そのContextを提供するモジュールごとMock化する必要があります。
ここでは、Child1, Child2コンポーネントがParentコンポーネントから提供されるContextを使用しているとします。
ParentコンポーネントのContextをMock化するために、jest.mockを使用し、Parentコンポーネント全体をMock化します。
こうすることで、ParentContextが提供する値をMock化することができます。
テスト対象のコード
Parent.jsx
export const ParentContext = React.createContext();
const Parent = () => {
const value1 = "value1";
const value2 = "value2";
return (
<ParentContext.Provider value={{ value1, value2 }}>
<Child1 />
<Child2 />
</ParentContext.Provider>
);
}
export default Parent;
Child1.jsx
import { ParentContext } from './Parent';
const Child = () => {
const { value1 } = useContext(ParentContext);
return <div>{value}</div>;
}
Child2.jsx
import { ParentContext } from './Parent';
const Child = () => {
const { value2 } = useContext(ParentContext);
return <div>{value}</div>;
}
テストコード
import { ParentContext } from './Parent';
jest.mock('./Parent', () => {
const originalModule = jest.requireActual('./Parent');
return {
__esModule: true,
...originalModule,
ParentContext: {
_currentValue: { value1: 'mockValue1', value2: 'mockValue2' },
},
};
});
今後の課題
ContextのMock化の例では、親コンポーネントを丸ごとMock化しているため、Contextが提供する値が多くなればなるほど前もって用意しなければならないダミーデータが増え、テストコードが複雑になるという問題がありました。
さらに効率的な方法を模索するために、React hooksおよび単体テスト手法について学習を進めていきたいと思います。
おわりに
rexcornuでは、これからの会社の成長を共にしてくれる仲間を募集しています。