🤖

Jest のモックを理解して何となくで扱わないようにする

2024/03/21に公開

この記事について

Jest のモックに関わるコードを書く上で行き詰まり、モックをあまり理解をせずを扱ってしまっていることに気づいた。
それをきっかけに Jest のモックについて、公式ドキュメント、書籍、技術記事を通して学び、できるだけコンパクトかつ、自分自身にとって理解しやすいと思える内容にまとめた記事。

モックとは

モック(テストダブル)とは、テスト対象の外部依存性を模倣するために用いられるオブジェクトのこと。
これによりテストを効率化できる。
例えば、 Web API で取得したデータを扱う場合、テストしたい対象は Web API そのものではなく「取得したデータに関連する処理」なのでこの代用品としてモックを使うことができる。

モック用語の整理

Jest のモックを理解するためには、以下の2種類のモック用語について理解する必要がある。

スタブ

代用」を行うオブジェクトのこと。
何らかの定められた値を返却する。

スパイ

記録」を行うオブジェクトのこと。
呼び出された回数、実行時引数などを記録する。

Jest のモック

Jest においてスタブ、スパイは以下のAPIを使い実現できる。

  • モックモジュール
    • jest.mock
  • モック関数
    • jest.fn
    • jest.spyon

ただし、ややこしいことに上述したモック用語の定義に忠実に沿った API は用意されていない。
jest.mock, jest.spyon と上述したモック、スパイはイコールではないことに注意

モックモジュールとモック関数

モックモジュールとモック関数について、以下の module.ts がディレクトリ内の同階層に存在する状態を前提とし、テストコードを例示して説明する。

// module.ts

export const hoge = () => {
  return 'hoge'
}

export const fuga = () => {
  return 'fuga'
}

export const piyo = () => {
  return 'piyo'
}

モックモジュール

モジュールに対してスタブを適用することができる。

jest.mock

  • モジュールを指定し、メソッドにスタブを適用する
  • モック関数と組み合わせることでスパイも可能
import * as module from './module'

jest.mock('./module', () => ({
  ...jest.requireActual('./module'),
  hoge: () => 'mocked' // ①固定値を返すように準備
}))

describe('jest.mock', () => {
  afterEach(() => {
    // モックのステートをクリア 
    jest.resetAllMocks()
  })

  test('スタブされていること', () => {
    const result = module.hoge() // ②操作, ③入力
    expect(result).toBe('mocked') // ④アサート
  })

  // // 今回 hoge がモック関数ではないため、スパイされずこれは通らない
  // test('スパイされていること', () => {
  //   module.hoge() // ②操作
  //   expect(module.hoge).toHaveBeenCalled() // ③出力, ④アサート
  // })
})

モック関数

特定の関数に対してスタブ、スパイを適用することができる。

jest.fn

  • ゼロから新しいスタブを生成する
  • .mockReturnValue() などのメソッド使って返り値を設定できる
  • 呼び出し情報(回数、引数など)をスパイする

jest.fn を使って関数を上書きし、スタブを生成した後、 jest.spyOn のように元の関数の状態に戻すメソッドは用意されていない

import * as module from './module'

describe('jest.fn', () => {
  afterEach(() => {
    // モックのステートをクリア
    jest.resetAllMocks()
  })

  const fn = ((module.fuga as jest.Mock) = jest.fn()) // ①固定値を返すように準備

  test('スタブされていること', () => {
    fn.mockReturnValue('mocked') // ①固定値を返すように準備(再設定)
    const result = module.fuga() // ②操作, ③入力
    expect(result).toBe('mocked') // ④アサート
  })
  
  test('スパイされていること', () => {
    module.fuga() // ②操作
    expect(module.fuga).toHaveBeenCalled() // ③出力, ④アサート
  })
})

jest.spyOn

  • 関数の振る舞いは元の実装のまま、スタブを生成する
  • .mockReturnValue() などのメソッド使って返り値を設定できる
  • 呼び出し情報(回数、引数など)をスパイできる

jest.fn と異なり、mockRestore()を使うことで簡単に元の関数の状態に戻すことができる

import * as module from './module'

describe('jest.spyOn', () => {
  afterEach(() => {
    // jest.spyOn する前の元の関数に戻し、ステートを初期化する
    jest.restoreAllMocks()
  })

  test('スタブされていること', () => {
    const spy = jest.spyOn(module, 'piyo')
    spy.mockReturnValue('mocked') //①固定値を返すように準備
    const result = module.piyo() // ②操作, ③入力
    expect(result).toBe('mocked') // ④アサート
  })

  test('スパイされていること', () => {
    jest.spyOn(module, 'piyo') // ①記録の準備
    module.piyo() // ②操作
    expect(module.piyo).toHaveBeenCalled() // ③出力, ④アサート
  })
})

モックのリセット

テスト間での影響を避けるため、必要に応じてモックのリセットを行う必要がある。
モックのリセットを行うための基本的なメソッドは3種類ある。

mockClear

スパイの履歴を削除する。
具体的には以下の情報。

  • mockFn.mock.calls(モック関数が呼ばれた時の引数の履歴)
  • mockFn.mock.instances(モック関数によって生成されたインスタンスの履歴)
  • mockFn.mock.contexts(モック関数が呼ばれた時のthisコンテキストの履歴)
  • mockFn.mock.results(モック関数の戻り値や例外の履歴)

mockReset

以下を実施する。

  • mockClear と同じくスパイの履歴の削除
  • モック関数の全ての設定(実装、返り値など)をリセットし、何も返さないモック関数に戻す

mockRestore

jest.spyOn で生成されたモック関数を元の関数に戻し、スパイやスタブの設定を破棄する。

まとめ

  • モックはテスト対象の外部依存性を模倣するオブジェクトのこと
  • スタブは「代用」を行うオブジェクトのこと
  • スパイは「記録」を行うオブジェクトのこと
  • Jest の jest.mock, jest.fn, jest.spyOn という API を用いてスタブ、スパイを実現できる
  • モックのリセットを行うための基本的なメソッドは mockClear, mockReset, mockRestore

参考

Discussion