【1日1zenn - day20】vitestでuseStateやuseRefをmockする方法
最近UXデザイン側のタスクが多くて中々コードを書けていなかったのですが、
ちょっと落ち着いてコードを書いたら案の定詰まったのでサクッと振り返ります。
状況
- 言語
- React
- TypeScript
- Vitest
- 実装内容
- モーダルを下スワイプすることで閉じるhooks
- スワイプした距離がモーダルの高さの一定以上だったらhandleCloseを作動させる
- 元々あるモーダルに適用させていたが、ほかのモーダルにも実装することになったため、
src/hooks
配下の共通hooksとして切り出した
- モーダルを下スワイプすることで閉じるhooks
- 事象
- 以下のエラーが出るようになった
TypeError: Cannot redefine property: useState
TypeError: mockUseRef.mockReturnValue is not a function
- 以下のエラーが出るようになった
Cannot redefine property: useState について
実装として、スワイプしたときに最初に触れた座標をuseStateで保持しており、そのSetterが正しく呼び出されているかテストする際に発生しました。
import * as react from 'react'
describe('useSwipeDownToClose', () => {
describe('handleHoge', () => {
context('~~~のとき', () => {
it('期待するアクションがよばれること', () => {
vi.spyOn(react, 'useState').mockImplementation(() => [ //ここで上記エラー発生
0,
mockSetTouchStartY
])
})
})
})
})
やりたいことはuseStateの実装を保持しつつ、呼び出されたときの戻り値としてmock化したsetterを渡すことです。
よってuseStateをspyし、mockImplementationで戻り値を指定すればいいはずだと思っていました。
exportされたファイルをspyするためには名前空間でimportしてspyOnに渡す必要があるので上記のような書き方にしてます。
ちなみにspyとmockの差分は以下の認識なので、今回はspyを選んでます。
- 元の実装の保持とか関係なく、ただ呼び出しなどの監視さえできればいい場合: mock
- 引数で渡す関数などであればこれで良かったりする
- 元の実装を保持したい場合: spy
- 引数で渡す関数ではなく、テストしたいhooks内で生成される関数の場合はspyが必要
- useStateを引数で渡すよう実装を変えればmockにして解決できたと思うが、共通メソッドかつ今回のuseStateがこのhooks内でのみ使われるものなので外に出したくなかった
しかしこの書き方だと、Cannot redefine property : useState
というように、useStateを再定義できないエラーが出ました。
要因と解決策
結論から言うと以下のようにすることで直りました。
import * as react from 'react'
// 以下でreactの実装を保持したままmockする
vi.mock('react', async () => {
const actual = await vi.importActual<typeof import('react')>('react')
return {
...actual,
}
})
describe('useSwipeDownToClose', () => {
describe('handleHoge', () => {
context('~~~のとき', () => {
it('期待するアクションがよばれること', () => {
vi.spyOn(react, 'useState').mockImplementation(() => [
0,
mockSetTouchStartY
])
})
})
})
})
まだざっくりの理解なのですが、useStateをmockできていないせいで、生のuseState自体の実装を上書きしようとしていてこのエラーが出ていたっぽいです。
関連しそう?なことメモ
mockUseRef.mockReturnValue is not a function
これはモーダルの高さとスワイプした距離を比べてcloseするか判定するために、それぞれのモーダルの高さをrefから取得するuseRefです。
元の実装は以下でした。
const mockUseRef = vi.mocked(useRef)
describe('useHandleClose', () => {
beforeEach(() => {
const mockModalRef = {
current: { offsetheight: 600, },
} as RefObject<HTMLDivElement>
mockUseRef.mockReturnValue(mockModalRef)
})
})
これも要因としては同様にmock化できていなくて、2つやったら直りました。
解法①
mockUseRefの宣言を以下にしました。
const mockUseRef = vi.spyOn(react, 'useRef')
これはuseState同様に、mockUseRefをspyする形です。
元の実装でconsoleを仕込むと以下のようになりました。
const mockUseRef = vi.mocked(useRef)
console.log('mockUseRefは?', mockUseRef)
//出力結果
mockUseRefは? [Function: useRef]
mockUseRefの中に、mock用のメソッドが追加されていないことがわかります。
これだとmockUseRef.mockReturnValue(mockModalRef)
みたいに呼び出しても、mockUseRefの中にそんなメソッドねえよ、とエラーが返されるわけです。
一方spyすると以下のような出力になりました
const mockUseRef = vi.spyOn(react, 'useRef')
console.log('mockUseRefは?', mockUseRef)
//出力結果
mockUseRefは? [Function: useRef] {
getMockName: [Function (anonymous)],
mockName: [Function (anonymous)],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
getMockImplementation: [Function (anonymous)],
mockImplementation: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
withImplementation: [Function: withImplementation],
mockReturnThis: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)]
}
ちゃんとmock系のメソッドも生えてますね。
解法②
こっちは正直理解できていないのですが、とりあえず最終系は以下です。
describe('useHandleClose', () => {
beforeEach(() => {
const mockUseRef = vi.spyOn(react, 'useRef') //ここに書いた
const mockModalRef = {
current: { offsetheight: 600, },
} as RefObject<HTMLDivElement>
mockUseRef.mockReturnValue(mockModalRef)
})
})
なんか一生テスト実行時にuseRefの戻り値がmockModalRefにならなくて、nullチェックに引っかかって普通にテスト落ちていたのですが、上記のようにグローバル変数ではなくdescribe内でbeforeEachしたら通るようになりました。またcontext内で書いても直ったのですが、逆にdescribeでbeforeEachせずに書いた場合は設定できなかったです。
これもきっと何か要因があるはずなのですが、そこまで辿り着けず。。
TODOメモ
全然理解できてなくて、このままだとまた沼ると思うので、土日とかで理解しに行きます。
一旦詰まった時の引き出しとしてちゃんと保存しておきます。
Discussion