LoginSignup
9
4

More than 1 year has passed since last update.

Jestでglobalなオブジェクトをmockしたテストを書く

Last updated at Posted at 2021-12-21

この記事は Ateam Lifestyle Inc. Advent Calendar 2021 22日目の記事です。

タイトル通り、Jestでglobalなオブジェクトをmockしたテストを書く方法です。

globalオブジェクトを使用するコードのサンプル

下記のような位置情報を取得する簡単なhooksを書いてみました。

use-geolocation.ts
import { useEffect, useState } from 'react'

export const useGeolocation = (): GeolocationPosition | null => {
  const [geo, setGeo] = useState<GeolocationPosition | null>(null)

  useEffect(() => {
    navigator.geolocation.getCurrentPosition(setGeo)
  }, [])

  return geo
}

まずは何も考えずに愚直にこのhooksのテストを書いてみます。

use-gelocation.spec.ts
import { useGeolocation } from './use-geolocation'
import { renderHook } from '@testing-library/react-hooks'

const geolocationPosition = {
  coords: {
    accuracy: 1,
    altitude: null,
    altitudeAccuracy: null,
    heading: null,
    latitude: 1,
    longitude: 1,
    speed: null
  },
  timestamp: 1
}

describe('useGeolocation', () => {
  it('must return geolocation', () => {
    const { result } = renderHook(() => useGeolocation())
    expect(result.current).toEqual(geolocationPosition)
  })
})

このテストはnavigator.geolocation.getCurrentPosition(setGeo)の箇所で失敗します。
このようなAPIへのアクセスは結果をこちらでコントロールすることが出来ないのでmock化するのが定石です。

jest.spyOnを使用したやり方

axiosなどのようなnpmパッケージを使用する場合はjest.mockを使用してモジュール自体をmock化すればいいですが、今回のようなnavigatorや、他にはwindowconsolesetTimeoutなどはモジュールを指定できません。
なので今回はjest.spyOnを使用し、ドキュメントにある通りmockImplementationを使用して元の関数を上書きします。

const originalNavigator = { ...navigator }
const originalGeolocation = { ...navigator.geolocation }
const navigatorSpy = jest.spyOn(global, 'navigator', 'get')
navigatorSpy.mockImplementation(() => ({
  ...originalNavigator,
  geolocation: {
    ...originalGeolocation,
    getCurrentPosition: (successCallback) => {
      successCallback({
        coords: {
          accuracy: 1,
          altitude: null,
          altitudeAccuracy: null,
          heading: null,
          latitude: 1,
          longitude: 1,
          speed: null
        },
        timestamp: 1
      })
    }
  }
}))

navigatorSpy.mockImplementationで元々のオブジェクトの関数を定義しつつmock化したい箇所だけを上書きします。
このときTypeScriptだとNavigatorの型定義に従って内容をサジェストをしてくれるのでタイポやコールバックの引数の内容を漏らすのを防いでくれます。

元々のオブジェクトの関数をoriginalNavigator, originalGeolocationとして取り出しているのは、そのまま展開しようとするとnavigatorを呼び出したところでまたmockを作成しようとして無限ループになってしまうのでそれを防ぐためです。

また、jest.spyOnでmock化されたものを元に戻すためにテスト後はmockRestore()を呼び出しておきます。

navigatorSpy.mockRestore()

これでテストを書く準備は完了です。

実際のコード

上記を用いて出来た実際のテストコードがこちらです。
今回はテストケースが1パターンしかありませんが複数のテストケースが作成されることも考慮してmockの作成をbeforeEachに、mockRestore afterEachに書いています。

use-geolocation.spec.ts
import { useGeolocation } from './use-geolocation'
import { renderHook } from '@testing-library/react-hooks'

const geolocationPosition = {
  coords: {
    accuracy: 1,
    altitude: null,
    altitudeAccuracy: null,
    heading: null,
    latitude: 1,
    longitude: 1,
    speed: null
  },
  timestamp: 1
}

describe('useGeolocation', () => {
  let navigatorSpy: jest.SpyInstance<Navigator, []> | undefined

  beforeEach(() => {
    const originalNavigator = { ...navigator }
    const originalGeolocation = { ...navigator.geolocation }
    navigatorSpy = jest.spyOn(global, 'navigator', 'get')
    navigatorSpy.mockImplementation(() => ({
      ...originalNavigator,
      geolocation: {
        ...originalGeolocation,
        getCurrentPosition: (successCallback) => {
          successCallback(geolocationPosition)
        }
      }
    }))
  })

  afterEach(() => {
    navigatorSpy?.mockRestore()
  })
  
  it('must return geolocation', () => {
    const { result } = renderHook(() => useGeolocation())
    expect(result.current).toEqual(geolocationPosition)
  })
})

その他のやり方

やり方を調べているときに見かけた他の方法についても載せておきます。
ただし、いずれのやり方もグローバル空間を汚染するものであるため他のテストケースに影響を与える可能性があります。
なので基本的にはspyOnを使いテスト終了後にmockRestoreがいいと思います。

global.navigator.geolocationを上書き

navigator.geolocationをmockで上書きする方法です。

const mockGeolocation = {
  getCurrentPosition: jest.fn((successCallback: PositionCallback) => {
    successCallback({
      coords: {
        latitude: 1,
        longitude: 1,
        // ...省略
      },
      timestamp: 1
    })
  })
};

global.navigator.geolocation = mockGeolocation

TypeScriptだと下記のようにエラー表示になりますがJavaScriptの場合は何も出ずに普通に書けてしまうため気をつけなくてはなりません。
スクリーンショット 2021-12-16 20.14.42.png
ちなみにTypeScriptでも実行すれば動きはします。

Object.definePropertyを使用したやり方

Object.defineProperty(global, 'navigator', {
  get: () => {
    return {
      geolocation: {
        getCurrentPosition: (successCallback: PositionCallback) => {
          successCallback({
            coords: {
              latitude: 1,
              longitude: 1,
              // ...省略
            },
            timestamp: 1
          })
        }
      }
    }
  }
})

navigatorにアクセスしたときに取得できる内容を書き換えるという方法です。
こちらは特にエラーなども出ないので他に影響を与える可能性があることに気づきにくいです。
また、TypeScriptでも元々の型定義によらず完全に自由に定義できてしまうため型の恩恵を受けることができません。

感想

navigator.geolocation.getCurrentPositionのテストの仕方がわからないと聞かれて軽い気持ちで「mockすればいいじゃん」と言ったものの、いざ自分が書こうとしたらjest.mockで書けないため意外とすぐには出来ず調べたので記事にして共有することにしました。

明日は @yutaroud さんです。

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4