🤖

【Test Double】JestでMockとStubを確認する

2021/07/18に公開

はじめに

MockとStubの違いをソースコードレベルで理解出来ていないと思い、記事を書いて整理してみました。色々検索して調べましたが、もし間違えている箇所がありましたら、ご指摘いただけると有り難いです。

Test Doubleとは

Test Doubleは、テストの対象が他のモジュール(クラスや関数など)に依存している場合、その代役として使われるモジュールのことです。

TestsDoublesの作成者であるGerardMeszarosによると、Test Doubleは5つのカテゴリに分類できます。その中でも今回は、JestでMockとStubについて書きます。

http://xunitpatterns.com/Test Double.html

テスト対象のシステム(SUT)

テストを行う対象のシステムのことをsystem under test(SUT)と言います。MockやStubのテストコードを確認する前に、まずはテストを行う対象のシステムを確認します。

src/fetchAlbums.ts

import axios from 'axios'

export const fetchAlbums = async (argument: string) => {
  const url = 'https://jsonplaceholder.typicode.com/'

  const response = await axios.get(`${url}${argument}`)
  return response.data
}

Axiosを使ってREST APIで取得した情報をテストします。APIサーバーは、JSONPlaceholderというサービスを使っています。

https://jsonplaceholder.typicode.com/albums にアクセスすると分かりますが、アルバムに関するデータを呼び出し、userId id title の3つの要素のJSONオブジェクトを取得します。このプログラムをもとにテストを行います。

モックデータの作成

まず最初にテストの実行に必要なモックデータの作成を行います。単体テストでは、外部の依存関係にあるモジュールは呼び出しません。テストのために、本番DBに新しいレコードを作成したり、APIリクエストの接続制限の回数を超えたりすることは望ましくないため、モックを作成します。

src/__mocks__/axios.ts

export default {
  get: jest.fn(() => Promise.resolve({
    data: [
      {
        userId: 1,
        id: 1,
        title: "quidem molestiae enim"
      },
      {
        userId: 1,
        id: 2,
        title: "sunt qui excepturi placeat culpa"
      }
    ]
  }))
}

Jestはテスト対象ファイルと同じディレクトリに、__mocks__ディレクトリを作成することで、モック対象のファイルを検出し依存関係を自動的にモックします。そして、モック関数jest.fn()を定義することで、どのような引数で何回呼ばれたかなどを記録します。

そうすると、Jestを実行した際にresponseは実際のAPIのエンドポイントの結果を返すのではなく、__mocks__に定義した値を返します。

試しaxiosのgetメソッドの引数を空文字にして、responseの結果を出力してみます。

import axios from 'axios'

export const fetchAlbums = async () => {
  const response = await axios.get('')
  console.log(response) // data: [{ userId: 1, id: 1, title: 'quidem molestiae enim' }, { userId: 1, id: 2, title: 'sunt qui excepturi placeat culpa' }]
  return response.data
}

console.log(response)を見ると__mocks__に定義した値を返していることが確認出来ました。

Mock

Mockは、送信メッセージのテストです。関数の実行回数、関数が正しい引数で呼び出されたか、メソッドの呼び出しの順序などの期待する結果が得られる過程を検証します。

tests/fetchAlbums.test.ts

import { fetchAlbums } from "../src/fetchAlbums"
import mockAxios from 'axios'

describe('fetchAlbums.ts', () => {
  test('fetch the albums', async () => {
    await fetchAlbums()

    const url = 'https://jsonplaceholder.typicode.com/'
    const argument = 'albums'

    expect(mockAxios.get).toHaveBeenCalledTimes(1)
    expect(mockAxios.get).toHaveBeenCalledWith(`${url}${argument}`)
  })
})

__mocks__でjest.fn()を定義したことにより、toHaveBeenCalledTimesやtoHaveBeenCalledWithなどのメソッドが機能します。

toHaveBeenCalledTimes
axios.getが呼ばれた回数を確認します。

toHaveBeenCalledWith
指定した引数(argument)を与えられて、呼び出されたことを確認します。

Stub

Stubは、受信メッセージのテストです。テストに必要だけどまだ実装出来ていないモジュールがある時に、そのモジュールの代わりにテストケースに沿った値を返してくれるテスト用のプログラムです。

関数を呼び出して決まった値を返すかを検証します。Mockと異なり、関数の実行回数、関数が正しい引数で呼び出されたかなどの過程には干渉せず、結果だけを検証します。

tests/fetchAlbums.test.ts

import { fetchAlbums } from "../src/fetchAlbums"

describe('fetchAlbums.ts', () => {
  test('fetch the albums', async () => {
    const fetchedAlbums = await fetchAlbums('albums')

    const response = [
      {
        userId: 1,
        id: 1,
        title: "quidem molestiae enim"
      },
      {
        userId: 1,
        id: 2,
        title: "sunt qui excepturi placeat culpa"
      }
    ]

    expect(fetchedAlbums).toEqual(response)
  })
})

__mocks__に定義したレスポンスのオブジェクトと、testファイルに作成したresponsのオブジェクトを突き合わせています。

そのため、実際に使用するAPIがまだ実装出来ていなくても、Stubによって受信メッセージのテストを行うことが出来ます。

toEqual
fetchedAlbumsresponseの2つのObjectを比較します。

参考

https://goyoki.hatenablog.com/entry/20120301/1330608789
https://gotohayato.com/content/483/
https://crieit.net/posts/Mockito
https://phpunit.readthedocs.io/ja/latest/test-doubles.html#
https://medium.com/rd-shipit/test-doubles-mocks-stubs-fakes-spies-e-dummies-a5cdafcd0daf

Discussion