はじめに
こんにちは、Gakken Leap のフロントエンドエンジニアの日下です。
この度、nuxt3 のテストを vitest で書く機会があったのですが、nuxt から提供される useFetch や navigateTo 等の関数のモックができずハマってしまいました。
公式ドキュメントや記事も調べたのですがなかなか情報が見つけられず、解決まで時間が取られてしまったため、備忘録も兼ねて解決策を書き残しておきたいと思います。
ハマった部分
今回テストを書くにあたりリグレッションが発生したらすぐに検知できるようにというモチベーションがあったため、ページ単位で複数のコンポーネントを組み合わせた結合テストを書き始めました。
ページ全体での動きをテストする場合、api コールのための useFetch 関数や画面遷移のための navigateTo 関数のモックが必要になります。
公式ドキュメントによると以下のように mock が書けます。
import { afterEach, describe, expect, it, vi } from 'vitest'
function getLatest(index = messages.items.length - 1) {
return messages.items[index]
}
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // can also be a `getter or setter if supported`
}
describe('reading messages', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('should get the latest message with a spy', () => {
const spy = vi.spyOn(messages, 'getLatest')
expect(spy.getMockName()).toEqual('getLatest')
expect(messages.getLatest()).toEqual(
messages.items[messages.items.length - 1],
)
expect(spy).toHaveBeenCalledTimes(1)
spy.mockImplementationOnce(() => 'access-restricted')
expect(messages.getLatest()).toEqual('access-restricted')
expect(spy).toHaveBeenCalledTimes(2)
})
it('should get with a mock', () => {
const mock = vi.fn().mockImplementation(getLatest)
expect(mock()).toEqual(messages.items[messages.items.length - 1])
expect(mock).toHaveBeenCalledTimes(1)
mock.mockImplementationOnce(() => 'access-restricted')
expect(mock()).toEqual('access-restricted')
expect(mock).toHaveBeenCalledTimes(2)
expect(mock()).toEqual(messages.items[messages.items.length - 1])
expect(mock).toHaveBeenCalledTimes(3)
})
})
}
vi.spyOn()関数を使うことで関数のモックができます。第一引数にオブジェクトを渡し、第二引数にメソッドを渡します。
モックを行うとテスト中に実際の関数の代わりに mock した関数が呼び出されるようになります。
上記の例では呼び出された回数を取得していますが、以下のように書くことで関数の中身を上書きすることもできます。api を mock するときはモックデータを返すように書きます。
vi.spyOn().mockImplementation(() => {});
では useFetch 等の nuxt から提供される関数を mock する場合はどのようになるでしょうか。
第二引数に'useFetch'を渡すというところまではわかりやすいのですが、第一引数に何を渡せば良いのかという部分で詰まってしまいました。
nuxt3 では useFetch 等の nuxt から提供される関数は import 文を書かなくても利用できます。そのため、普段実装するときはあまり意識することは少ないかと思います。
明示的に書く場合は以下のように書きます。
import { useFetch } from "#app";
しかし、第一引数に#app と書いても意味がありません。#app
はエイリアスのようなもので実行時に nuxt 側で実際のパスに変換されるため、テスト環境ではこの方法で参照できません。
このパスが指し示すオブジェクトが何なのかという点がポイントなのですが結論としては以下のように書くことで解決できました。
import * as nuxt from "nuxt/app";
vi.spyOn(nuxt, "useFetch");
#app
が参照している nuxt オブジェクトを第一引数に渡すことが必要でした。
navigateTo 等の関数も同様の手段でモックできます。
おわりに
振り返ってみれば単純なことなのですが、ネットに nuxt の情報が少ないということもありこの方法に行き着くまでにだいぶ時間がかかってしまいました。
同じように nuxt3 にテストを導入しようとしている方の一助になれば幸いです。
エンジニア募集中
Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています!!
ぜひお気軽にカジュアル面談へお越しください!!