💭

【1日1zenn - day3】React + TypeScriptのmockとかの作り方あれこれ

2024/12/12に公開

1日1zenn用のアウトプット。

うちはテストしやすいようにhooksを切り分けているので、ありがたいことにテストケースを書くこと自体はそんなに大変じゃないですが、自分の場合は適切なmock化にかなり沼ったりします。
テスト自体は障害を起こさないためにも重要だとして、ここに時間をかけすぎるのは投資対効果が悪い。
よってこの場でまとめながら理解していきます。

縦横無尽に書いて行くので、どこかで体系的に整理し直すかも。

いい記事を探す

一番感謝した記事

https://qiita.com/YSasago/items/6109c5d3fbdbffa31c9f
ドンピシャで自分の知りたかったことがまとまってました。
パンチラインを引用してメモらせてください。

モック関数の戻り値を変更するには、mockImplementation

mock.test.js
it("return `Hoge`", () => {
   const mockFunction = jest.fn().mockImplementation(() => "Hoge"); // mockFunction関数の返り値にHogeを設定
   expect(mockFunction()).toBe("Hoge");
 });

省略することも可能です。

mock.test.js
it("return `Hoge`", () => {
   const mockFunction = jest.fn(() => "Hoge");
   expect(mockFunction()).toBe("Hoge");
 });

これを使うコードの例として、ちょっと以下を考えてみます。
※形だけそれっぽくしたコードです。

hooks.test.ts
import * as Hooks from '../hooks'

const mockHandleClick = jest.fn()

describe('useHandleOpenProfiele', () => {
  let spyUseHandleClick: jest.spyInstance

  beforeAll(() => {
    spyUseHandleClick = jest
      .spyOn(Hooks, 'useHandleClick')
      .mockImplementation(() => mockHandleClick)
  })

  afterAll(() => {
    spyUseHandleClick.mockRestore
  })

  it('期待するアクションがよばれること', () => {
    const { result } = renderHook(useHandleOpenProfiele)
    result.curent()

    expect(mockHandleClick).toHaveBeenCalledWith(userId)
  })
})

ここでやっている要素と雑多な言語化は以下です

  • Hooksとしてhooksの中身をインポート
    • spyOnはオブジェクトとメソッド名を引数にとります(docs)
    • 第一引数に'../hooks'のようなパスを指定できるわけじゃないので、hooks.tsをテストしたい場合は、Hooksのようにオブジェクトとしてインポートします
      • こういうのが必要なのは、hooksの中に依存関係がある複数のメソッドがあり、それら一つ一つを分けてmock化しないとテストケースがうまく実行されない場合でしょう
      • 付随して、beforeAllやafterAllでmock関数の設定と解除を行なっています
      • なおjest.spyInstanceはTypeScriptでspy関数の型エラーを出さないための型らしいです
  • beforeAllでspyUseHandleClickを、hooks.tsのuseHandleClickのスパイ関数にした上で、その戻り値をmockUseHandleClickに設定する
    • こうすることでこのテストケースの監視対象であるmockHandleClickが、正しくuseHandleClickの実行結果として定義されます。
    • 多分useHandleClickのコールバック関数がhandleClickで、それをテストしたいときとかはこういう実装をするのかな。
  • afterAllで上記スパイの設定を解除する
    • useHandleClickが同じhooks.ts内の他のメソッドで使われている場合は、同じファイルでテストするなら解除しないと他のメソッドがうまく作動しません
      • スパイ関数の戻り値はundefinedなので。

要はコールバック関数を監視したい時などにはmockImplementationを使う、みたいなイメージですかね。

他の戻り値の設定

引数に応じて異なる処理をしない場合はmockReturnValue

超雑ですが、たとえば以下のようなとき。

hooks.ts
// なんかクリックした時にuserIdを保持してたらそれを詰めた状態でuserIdとsetterを返すとか
export const useHandleClick = (userId: string | null) => {
  const [userId, setUserId] = useState<string | null>(null)
  setUserId(userId)
  return (
    userId,
    setUserId,
  )
}

こういう処理の場合は、userIdがnullだろうがstringだろうがそれを詰める感じなので、引数で処理が異なることはないですね。
んーこの理解で合ってるのだろうか。例が雑すぎて微妙。

非同期処理の戻り値をmockしたい場合はmockResolveValue

これはそのままですね。
awaitしてAPIに値を取りに行った結果をmockしたい場合とかはこれを使いそう。

雑に色々パターン出し

同じファイルで依存関係がないメソッドをそれぞれmockする

hooks.test.ts
jest.mock('../hooks', () => {
  useHandleClickIcon: jest.fn(),
  useHandleClickButton: jest.fn(),
})

こうすると同じhooks内のそれぞれのメソッドを別々にmock化できる。
もしuseHandleClickIconの処理にuseHandleClickButtonが使われている場合は、useHandleClickButtonの返り値はundefinedになるので機能しなくなるためそういう時はspyOnの指定を使う。

共通エクスポートの中の一つをmockする

hooks.test.ts
import { useSetAtom } from 'jotai'

const mockHandleClickUserId = jest.fn()

jest.mock('jotai', () => {
  ...jest.requireActual('jotai'),
  useSetAtom: jest.fn(() => mockHandleClickUserId),
}))

これはuseSetAtomの戻り値をmockHandleClickUserIdにしている例です。mockImplementationとかの省略形ですね。
このとき、jotaiでエクスポートされるuseSetAtom以外もmockにされたらuseSetAtomが動かなくなるので、...jest.requireActual('jotai')と書くことで他のメソッドの動作は本来の動きに担保しつつ、useSetAtomに関してはmockHandleClickUserIdを返すように設定します。

元の関数の動きを維持したままmockにする

hooks.test.ts
const mockHandleClick  = HandleClick as jest.mock
mockHandleClick.mockReturnValue(testUserId)

これはHandleClickの動きを維持したまま、返り値を指定したい場合などに使います。
mockHandleClick = jest.fn()だとhandleClickの動きを使えないし、mockHandleClick = HandleClickだと型エラーになるときなどに使うっぽいです。

おわりに

かなり雑多に書いたので、どこかのタイミングでまとめ直します。
あと新しい指定とかあれば追記していきます。

Discussion