この記事は Ateam Lifestyle Inc. Advent Calendar 2021 22日目の記事です。
タイトル通り、Jestでglobalなオブジェクトをmockしたテストを書く方法です。
globalオブジェクトを使用するコードのサンプル
下記のような位置情報を取得する簡単なhooksを書いてみました。
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のテストを書いてみます。
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
や、他にはwindow
やconsole
やsetTimeout
などはモジュールを指定できません。
なので今回は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
に書いています。
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の場合は何も出ずに普通に書けてしまうため気をつけなくてはなりません。
ちなみに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 さんです。