見出し画像

【Jest】モック関数の使い方を用途ごとにまとめてみた。


はじめに


こんにちは、株式会社SHIFT DAAE(ダーエ)テクノロジーグループのイケモトです。業務上でJestのモック関数に触れていた際に、色々なことができる反面、自分が実装したい内容に対してどのようにモック関数を使えばいいのか迷うことがあったため、自分なりにモック関数の使い方をまとめてみました。

開発環境


  • Windows10 22h2

  • Next.js 14.0.4

  • React 18

  • Jest 29.6.4

  • Visual Studio Code 1.84.1

前提


Next.jsのプロジェクトを作成する

ここではNext.jsのプロジェクトを作成するまでの環境構築については触れません。
Next.jsのドキュメントを参考に作成しましょう。
https://nextjs.org/docs/getting-started/installation

Jestを導入する

用意したNext.jsのプロジェクトにJestを動かすのに必要なライブラリをインストールします。
こちらもNext.jsのドキュメントにやり方が記載されています。
https://nextjs.org/docs/app/building-your-application/testing/jest

関数の呼び出しやコールバックを確認したい


関数がどのように呼び出されたか、何回呼び出されたかなどのテストを行いたい場合はjest.fn()を使います。
jest.fn()を用いることで、簡単にテスト環境で動作する関数を作成することができます。

下記のコードは、実際にjest.fn()を使ってButtonのコールバックが何回呼ばれているかを確認するコードです。

//src/components/button.tsx
export interface ButtonProps {
  onClick : (e: any) => any
}
//ボタン押下時のコールバックが動くかを確認したい。
const Button = ({
  onClick
} : ButtonProps) => {
  return <button onClick={onClick}/>
}
//src/__test__/fn.test.tsx
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import Button from 'src/components/button.tsx';

describe('jest.fn()のサンプル', () => {
  it('buttonのコールバック確認', async () => {
    const callback = jest.fn();
    render(<Button onClick={callback}/>)
    waitFor(() => expect(screen.getByRole("button")));
    
    const button = screen.getByRole("button");
    //ボタンの押下
    fireEvent.click(button);
    expect(callback).toHaveBeenCalledTimes(1);

    //さらに3回押下
    fireEvent.click(button);
    fireEvent.click(button);
    fireEvent.click(button);
    expect(callback).toHaveBeenCalledTimes(4);
  });
});

ボタン押下時のコールバックをjest.fn()を使って用意し、toHaveBeenCalledTimesでボタンを押した数だけコールバックとして実装したjest.fn()が呼び出されているかを確認しています。

関数の戻り値を固定したい


jest.fn()はundefinedを返すモック関数になります。
そのため、jest.fn()で作成したモック関数の戻り値を指定するためにjest.fn().mockImplementationを使います。

下記のコードはlevelを引数として挙動を変えるlevelCheckerをテストする場合のコードです。

//src/components/levelChecker.tsx
interface LevelCheckerProps {
    level : number
}

export const LevelChecker = ({ level } : LevelCheckerProps) => {
    const threshold = 25
    return 
        <div>
            {level > threshold ? "you are an expert" : "you are a beginner"}
        </div>
}
//src/__test__/mockReturnValue.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { LevelChecker } from 'src/components/levelChecker'

describe('mockReturnValueのテスト', () => {
    it("levelCheckerの動作確認", async () => {
        const getOneLevel = jest.fn().mockImplementation(() => 1);
        const getFiftyLevel = jest.fn().mockImplementation(() => 50);

        render(<LevelChecker level={getOneLevel()} />);
        waitFor(() => expect(screen.getAllByText("you are a beginner")));

        render(<LevelChecker level={getFiftyLevel()} />);
        waitFor(() => expect(screen.getAllByText("you are an expert")));
    });
});

また、jest.fn().mockImplementationのシンタックスシュガーとしてjest.fn().mockReturnValueが存在します。

//どちらも1の戻り値を返す
const getOneLevel = jest.fn().mockImplementation(() => 1);
const getOneLevel = jest.fn().mockReturnValue(1);

このシンタックスシュガーにはPromiseベースの関数も用意されています。

jest.fn().mockImplementationとjest.fn().mockReturnValueの使い分けについては以下の通りです。

  • 具体的な実装を行いたい場合は、jest.fn().mockImplementationを使用します。これにより、モック関数が呼び出されたときに指定した実装が実行されます。

  • 具体的な値を返すべき場合はmockReturnValue/mockResolvedValue/mockRejectedValueを使用します。これにより、モック関数が呼び出されたときに指定した値が返されます。

関数が呼び出されるたびに異なる戻り値を返したい


関数が呼び出されるたびに異なる戻り値を返したい場合は、jest.fn().mockReturnValueの代わりにjest.fn().mockReturnValueOnceを使うことで実装できます。
例えば、先ほどのlevelCheckerの動作確認の際に戻り値ごとにjest.fn().mockReturnValueで定義をしていましたが、mockReturnValueOnceを使うことで以下のように書き換えることができます。

//戻り値ごとに定義していたこれらを
const getOneLevel = jest.fn().mockReturnValue(1);
const getFiftyLevel = jest.fn().mockReturnValue(50);

//このように書き換えることでgetLevelだけで同じテストが可能となる。
const getLevel = jest.fn().mockReturnValueOnce(1)
                          .mockReturnValueOnce(50)

これでgetLevelを呼び出すたびに戻り値が1、50と順番に返ってきます。

ただしmockReturnValueOnceのみを使った場合、指定回数以降(上のコードでは3回目以降)はundefinedと戻り値が返されるため、それを防ぎたい場合はチェーンメソッドの最後にmockReturnValueを使ってデフォルトの戻り値を定義する必要があります。

オブジェクトの特定の関数をモック化したい


jest.spyOn()を使うことでオブジェクトの特定の関数をモック化することができます。
先ほどのlevelCheckerを変更して、generateオブジェクトのgeneratorLevelから返ってくる1~100までのランダムな数値を元に判断するような挙動に変えました。

//src/utils/generator.tsx
export const generator = {
    generateLevel () {
        return Math.floor(Math.random() * 99 + 1);
    }
}
//src/components/levelChecker.tsx
import { generator } from "src/utils/generator";

export const LevelChecker = () => {
    const level = generator.generateLevel();
    const threshold = 25
    return <div>
        {level > threshold ? "you are an expert" : "you are a beginner"}
    </div>
}

変更したlevelCheckerをjest.spyOn()を使ってテストしたコードが以下になります。

//src/__test__/spyOn.test.tsx
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { LevelChecker } from 'src/components/levelChecker';
import generator from 'src/utils/generator';

describe('jest.spyOn()のサンプル', () => {
  afterEach(() => {
    // モック化した全ての関数を元に戻すことも可能
    jest.restoreAllMocks();
  });

  it('generatorが100固定で返ってくることを確認する', () => {
    const spy = jest.spyOn(generator, "generateLevel").mockImplementation(() => 100);
    render(<LevelChecker/>);
    console.log(generator.generateLevel());
    waitFor(() => screen.getByText("You are an expert"));

    // jest.SpyInstanceを持っておけば、モック化した関数を戻せる
    // spy.mockRestore();
    });

  it('generatorが1固定で返ってくることを確認する', () => {
    //前のテストでmock化した内容をそのまま上書きすることもできる
    jest.spyOn(generator, "generateLevel").mockImplementation(() => 1); 
    console.log(generator.generateLevel());
    waitFor(() => screen.getByText("You are a beginner"));
  });
});

src/__test__/spyOn.test.tsxコード内にもコメントとして残してますが、一度モック化した関数はテストをまたいでモック化されます。
そのため、テストをまたいで同じ関数を利用する場合は、モック化をリセットするか上書きする必要があります。(もちろん、モック化した値をそのまま使いまわすこともできます。)

モジュールをモック化したい


モジュールのモック化をする場合はjest.mock()を使います。
Axiosのような外部モジュールをモック化したい場合にも利用できます。
下記のコードはAxiosを使ってサーバーからデータを取得し表示するコンポーネントです。

//src/components/userList.tsx
import axios from "axios";
import { useState, useEffect } from "react";

type User = {
    id : string,
    fullName : string,
}

export const userList = () => {
    const [userData, setUserData] = useState<User[]>([]);

    useEffect(() => {
        const getUserData = async () => {
            try {
                const response = await axios.get("/user.json");
                setUserData(response.data)
              } catch (e: any) {
                console.error(e);
              }
        }
        getUserData();
    }, [])

    return(
        <div>
            {userData && userData.map((user) => {
                return(
                    <div>
                        <div>
                            {user.id}
                        </div>
                        <div>
                            {user.fullName}
                        </div>
                    </div>
                )
            })}
        </div>
    )
}

このコンポーネントをテストしたくても、URLは適当に書いたものなのでデータが返ってきません。そのため、Axiosをモック化することで挙動を確認します。
下記のコードはAxiosをモック化してuserList.tsxをテストしたものです。

//src/__test__/mock.test.tsx
import { UserList } from "src/components/userList";
import axios from "axios";
import { render, waitFor, screen } from "@testing-library/react";

//axiosのモック化
jest.mock("axios");

describe("userList", () => {
    it("axiosからテスト用のデータを取得する", async () => {
        //jest.fnが割り振られたため、そのままだとundefinedが戻り値になる。
        expect(axios.get('/user.json')).toBeUndefined();
        //実際に取得したいテスト用のデータを実装する。
        //axios.get.mockResolvedValueと書くと型エラーを起こすため、jest.Mockedを使って型定義をする
        (axios as jest.Mocked).get.mockResolvedValue({ data: [
            {
                id : "20050907",
                fullName : "シフト太郎"
            }
        ]});
        render();
        await waitFor(() => screen.getByText("シフト太郎"));
        expect(document.body).toMatchSnapshot()
    })
})

まず、jest.mock("axios");を使ってAxios自体をモック化します。
この時、注意する点としてjest.mock()は必ずdescribeの外に書かないと機能しません。
Axiosをモック化したことにより、Axios内の関数(axios.getやaxios.post)にjest.fn()が割り振られるようになります。
関数の戻り値を固定したい。 の項目で説明した通りjest.fn()はundefinedを戻り値とするため、mockResolvedValueを使ってテスト用のデータを実装しています。

最後にsnapshotを使って、テスト用のデータを使ってコンポーネントがレンダリングされているかを確かめています。

おわりに


いくつかのユースケースに分けてモック関数の使い方を説明してきました。
使い方を簡単にまとめると

  • 新しくモック関数を用意したい場合はjest.fn()

  • 特定のオブジェクトのメソッドをモック化したい場合はjest.spyOn()

  • モジュールをモック化したい場合はjest.mock()

と覚えておけば、モック関数を使うときに迷わなくなると思います。
モック関数が使えると単体テストを実装する際の幅が広がりますので、自分も頑張って使いこなせるようになりたいです。

参考資料


\もっと身近にもっとリアルに!DAAE公式Twitter/


執筆者プロフィール:Ikemoto Fumito
Unity・C#でのゲーム開発やKotlin・JavaでのAndroidアプリ実装を主に行ってました。
最近では、TS + Nextでのフロントエンド開発やPythonなどに携わっています。

お問合せはお気軽に

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら

PHOTO:UnsplashAles Nesetril