React + TS + JestでuseEffect内のPromise内の関数をspyOn

  • 理論上可能そうだったので、ひたすら試行錯誤してたらできたのでメモがてら
  • axios そのものをモックして、その結果に応じた処理が想定通り走っているかどうかを確認したい
  • create-react-app で作成した TypeScript React のプロジェクトに npm t を掛けてテストすることを想定
  • 但し検証用のプロジェクトは @lycolia/ts-boilerplate-generator-cli で作成

確認環境

EnvVer
react17.0.1
react-scripts4.0.2
typescript4.1.3

確認内容

検証対象

  • ページ読み込み時に一回だけ API を蹴ってなんかすることを想定

import axios from 'axios';
import { useEffect } from 'react';
export const AsyncHookExample = () => { useEffect(() => { axios .get('https://localhost/') .then((res) => console.log(res)) .catch((err) => console.error(err)); }, []); return <p />;
};

検証方法

  • axios をモックした上で、モック関数の Promise の解決を待ち、 Promise のコールバックを spyOn して検査する内容
  • モック関数の Promise の解決法を求めるのにハマりましたが、これは foo.mock.results[0].value に対して await expect(foo.mock.results[0].value).resolves としてやればいける
  • 細かいことはソースコードのコメント参照

import { render } from '@testing-library/react';
import axios from 'axios';
import { AsyncHookExample } from './AsyncHookExample';
// `axios` をモックにする
jest.mock('axios');
// `axios` のモックを取得
const mockedAxios = axios as jest.Mocked<typeof axios>;
// `console.log()` を `spyOn'
const spiedConsoleLog = jest.spyOn(console, 'log');
// `console.error()` を `spyOn'
const spiedConsoleError = jest.spyOn(console, 'error');
describe('AsyncHookExample', () => { it('resolve promise in Hook', async () => { // テストに期待する結果 // `axios` はモックなので値は適当 const beResult = { status: 200, data: null, }; // `axios` のモックが `resolve` する値を設定 mockedAxios.get.mockResolvedValue(beResult); // コンポーネントを `render` して `useEffect` を走らせる render(<AsyncHookExample />); // `axios.get()` が呼ばれたことを確認 expect(mockedAxios.get).toBeCalled(); // モックの結果を取得 const testResult = mockedAxios.get.mock.results[0].value; // `reject` された値が期待通りであることを確認 await expect(testResult).resolves.toEqual(beResult); // `useEffect` の中の `Promise` の中にある `console.log()` が呼ばれたことを確認 expect(spiedConsoleLog).toBeCalled(); }); it('reject promise in Hook', async () => { // テストに期待する結果 // `axios` はモックなので値は適当 const beResult = { status: 400, data: null, }; // `axios` のモックが `reject` する値を設定 mockedAxios.get.mockRejectedValue(beResult); // コンポーネントを `render` して `useEffect` を走らせる render(<AsyncHookExample />); // `axios.get()` が呼ばれたことを確認 expect(mockedAxios.get).toBeCalled(); // モックの結果を取得 const testResult = mockedAxios.get.mock.results[0].value; // `reject` された値が期待通りであることを確認 await expect(testResult).rejects.toEqual(beResult); // `useEffect` の中の `Promise` の中にある `console.error()` が呼ばれたことを確認 expect(spiedConsoleError).toBeCalled(); });
});

あとがき

  • mockResolvedValue のスコープやらモックの Promise を解決させるマッチャやら調べるのに手間取った
    • あとはそもそも Promise が解決した結果がどこに入るのかとか、兎に角あれこれ
  • expect(mockedAxios.get).toBeCalled() が通るのはすぐに気づいたので、なら出来るだろうとひたすら調べてた