nuxtのユニットテストでregisterEndpointを使用するときに気を付けること

NuxtのユニットテストでregisterEndpointを使用するときに気を付けることについてまとめました。

Nuxtのユニットテストを行う際、APIエンドポイントへのリクエストをモックする必要がある場面があります。その際にregisterEndpointを使用すると、モックデータを返すNitroエンドポイントを簡単に作成できます。

registerendpoint | Testing · Get Started with Nuxt v3

使用例

通常は以下のようにエンドポイントのハンドラーをモック関数にして、テストのbeforeEachvi.resetAllMocksを呼び出してテストごとにモックをリセットして利用するのが一般的かと思います。

import { registerEndpoint } from '@nuxt/test-utils/runtime'
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'

describe('API tests', () => {
  const mockHandler = vi.fn()

  beforeAll(() => {
    registerEndpoint('/api/test', mockHandler)
  })

  beforeEach(() => {
    vi.resetAllMocks()
  })

  it('取得に成功した場合のケース', async () => {
    mockHandler.mockReturnValue({ id: 1, name: 'John' })
    await expect($fetch('/api/test')).resolves.toEqual({ id: 1, name: 'John' })
  })

  it('取得に失敗した場合のケース', async () => {
    mockHandler.mockReturnValue(new Response(null, { status: 401 }))
    await expect($fetch('/api/test')).rejects.toMatchObject({ statusCode: 401 })
  })
})

問題となるケース

基本的な使用例では特に問題はありませんが、registerEndpointを特定のテストでのみ使用したい場合があります。その際、ハンドラーをモック関数にするまでもないと思うことがあります。

it('test1', async () => {
  const handler = vi.fn()
  registerEndpoint('/this-test-only', handler)
  handler.mockReturnValue('ok')
  await expect($fetch('/this-test-only')).resolves.toEqual('ok')
})

以下のように書けばよりシンプルになりますが、この書き方には問題があります。前のテストで登録したエンドポイントが次のテストでも登録されたまま残ってしまいます。

it('test1', async () => {
  registerEndpoint('/this-test-only', () => 'ok')
  await expect($fetch('/this-test-only')).resolves.toEqual('ok')
})

it('test2', async () => {
  // エンドポイントが未登録であることを前提としたテストが意図せず失敗してしまう
  await expect($fetch('/this-test-only')).rejects.toMatchObject({
    statusCode: 404,
  })
})

前のテストで登録した状態が次のテストに影響を与えるのは、テストの独立性を損なうため望ましくありません。

registerEndpointで登録したエンドポイントを解除する

registerEndpointの戻り値は、登録したエンドポイントを解除するための関数です。この関数を呼び出すことで、登録したエンドポイントを解除することができます。

it('test1', async () => {
  const unregister = registerEndpoint('/this-test-only', () => 'ok')
  await expect($fetch('/this-test-only')).resolves.toEqual('ok')
  unregister() // エンドポイントを解除
})

it('test2', async () => {
  // エンドポイントが未登録であることを前提としたテストは期待通りパスする
  await expect($fetch('/this-test-only')).rejects.toMatchObject({
    statusCode: 404,
  })
})

注意点

解除関数は必ず呼び出すようにする必要があります。以下のようにunregisterを呼び出す前のアサーションが失敗した場合、エンドポイントが解除されずに次のテストに影響を与える可能性があります。

it('test1', async () => {
  const unregister = registerEndpoint('/this-test-only', () => 'ok')
  // このアサーションが失敗する
  // AssertionError: expected 'ok' to deeply equal 'ng'
  await expect($fetch('/this-test-only')).resolves.toEqual('ng')
  // unregisterは呼び出されない
  unregister()
})

it('test2', async () => {
  // このテストは意図せずパスしてしまう(404ではなく'ok'が返される)
  await expect($fetch('/this-test-only')).rejects.toMatchObject({
    statusCode: 404,
  })
})

try-finallyで解除関数を確実に呼び出すようにしたり、また以下のようにunregisterが確実に呼ばれるようにするといいかもしれません。

const unregisters: (() => void)[] = []

afterEach(() => {
  // テスト終了後に必ず解除
  unregisters.splice(0).forEach((fn) => fn())
})

it('test1', async () => {
  unregisters.push(registerEndpoint('/this-test-only', () => 'ok'))
  // このアサーションが失敗する
  await expect($fetch('/this-test-only')).resolves.toEqual('ng')
})

代替案

エンドポイントが未登録であることを前提としたテストはそもそも不確実性があり失敗するケースが出てきます。以下のように必ずエンドポイントの結果を設定するのでもいいかもしれません。

it('test1', async () => {
  registerEndpoint('/this-test-only', () => 'ok')
  await expect($fetch('/this-test-only')).resolves.toEqual('ok')
})

it('test2', async () => {
  registerEndpoint('/this-test-only', () => new Response(null, { status: 404 }))
  await expect($fetch('/this-test-only')).rejects.toMatchObject({
    statusCode: 404,
  })
})

まとめ

registerEndpointを使用する際は以下を意識する

基本的な特性

  • 登録したエンドポイントは自動解除されない
  • 同じパス・メソッドで再登録すると上書きされる
  • テストの独立性を保つため適切な管理が必要

使用パターン

  • モック関数パターン(複数テストで使用)
    • ハンドラをモック関数にしてテストごとに異なる結果を返す
    • beforeEachでのvi.resetAllMocksと組み合わせて使用
  • 明示的解除パターン(単発使用)
    • try-finallyで解除関数を確実に呼び出す
    • アサーション失敗時でも登録が解除されることを確認
  • 一括管理パターン(複数の一時的エンドポイント)
    • afterEachで解除関数の配列をまとめて実行

注意すべき点

  • 解除処理なしでの一時的な登録は避ける
  • アサーション失敗時でも解除関数が呼ばれるようにする
  • エンドポイントの未登録状態を前提とするテストは不確実性がある
  • テストの実行順序への依存を避ける

確認事項

  • 登録したエンドポイントの解除方法を決めているか
  • 他のテストへの影響を考慮しているか
  • 可能な限りテストの独立性を保っているか