A Developer’s Guide to Unit Testing Nuxt 3 Server Routes



This content originally appeared on DEV Community and was authored by Doan Trong Nam

Testing is a critical part of building robust and reliable applications. While Nuxt 3 makes creating server routes incredibly simple, setting up a proper testing environment for them can seem a bit daunting. Fear not! With the power of vitest and @nuxt/test-utils, you can create a clean, efficient, and powerful testing suite for your server-side logic.

This guide will walk you through setting up unit tests for your Nuxt 3 server routes, from initial configuration to mocking dependencies and writing comprehensive tests.

1. Configuring Vitest for Your Nuxt Environment

The first step is to ensure vitest knows how to run your tests within a Nuxt context. This is crucial for auto-imports and other Nuxt-specific features to work correctly in your test files.

The @nuxt/test-utils package provides a handy defineVitestConfig function that simplifies this process. Here’s what a typical vitest.config.ts looks like:

// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  test: {
    // Enable the Nuxt environment.
    environment: 'nuxt',
    environmentOptions: {
      nuxt: {
        // You can specify a DOM environment for components testing if needed.
        domEnvironment: 'happy-dom',
      },
    },
  },
})

By setting environment: 'nuxt', you’re telling Vitest to use a custom environment that boots up a Nuxt instance, making all its magic available to your tests.

Read more at Official Nuxt Testing Doc

2. Creating a Global Test Setup File

To avoid repetitive mocking in every test file, it’s best to create a global setup file. Vitest can be configured to run this file before your test suite starts. A common location for this is test/setup.ts.

This file is the perfect place to mock global utilities, especially the h3 functions that power Nuxt’s server routes (defineEventHandler, readBody, etc.).

Here’s how you can create a reusable utility to mock h3 functions:

// test/setup.ts
import type { H3Event, EventHandlerRequest } from 'h3'
import { vi } from 'vitest'

type Handler = (event: H3Event<EventHandlerRequest>) => Promise<unknown>

export function useH3TestUtils() {
  const h3 = vi.hoisted(() => ({
    defineEventHandler: vi.fn((handler: Handler) => handler),
    readBody: vi.fn(async (event: H3Event) => {
      if (event._requestBody && typeof event._requestBody === 'string') {
        return JSON.parse(event._requestBody)
      }
      return event._requestBody || {}
    }),
    getRouterParams: vi.fn((event: H3Event) => event.context?.params || {}),
    getQuery: vi.fn((event: H3Event) => event.context?.query || {}),
  }))

  // Stub the global functions to support auto-imports in your tests
  vi.stubGlobal('defineEventHandler', h3.defineEventHandler)
  vi.stubGlobal('readBody', h3.readBody)
  vi.stubGlobal('getRouterParams', h3.getRouterParams)
  vi.stubGlobal('getQuery', h3.getQuery)

  return h3
}

Key Concepts:

  • vi.hoisted(): This lifts the mock to the top of the module, ensuring that any subsequent imports of h3 receive our mocked version.
  • vi.stubGlobal(): Since Nuxt relies on auto-imports, these functions are effectively global. This function replaces the global instance with our mock, making it available seamlessly in our test files.

3. Mocking the H3Event Object

Your server handlers receive an event object that contains all the request details (body, params, query, headers). To test your handlers effectively, you need a way to create mock versions of this event.

Let’s create a helper function for this in test/mocks/h3-event.ts.

// test/mocks/h3-event.ts
import type { H3Event } from 'h3'
import { merge } from 'lodash'

export const createMockH3Event = (
  partialEvent: Partial<H3Event> & {
    body?: Record<string, any>
    params?: Record<string, any>
    query?: Record<string, any>
  }
): H3Event => {
  const event = {
    node: {
      req: {
        headers: { 'content-type': 'application/json' },
        method: 'POST',
      },
    },
    context: {
      params: partialEvent.params || {},
      query: partialEvent.query || {},
    },
    // Our mock readBody function will look for this property
    _requestBody: partialEvent.body,
  } as unknown as H3Event

  // Deeply merge the partial event to allow for overrides
  return merge(event, partialEvent) as H3Event
}

This powerful helper lets you easily simulate any request scenario. Need to test a POST request with a specific body? Or a GET request with query parameters? This function has you covered.

4. Writing Your First Server Route Test

With the setup complete, you’re ready to write a test. Let’s use an example API route that calls an external service. We’ll look at test/server/api/test.post.test.ts.

// test/server/api/test.post.test.ts
import { describe, expect, vi, it, beforeEach } from 'vitest'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'

// Import our test utilities
import { useH3TestUtils } from '~/test/setup'
import { createMockH3Event } from '~/test/mocks/h3-event'

const { defineEventHandler } = useH3TestUtils()

describe('POST /api/test', async () => {
  // 3. Dynamically import the handler *after* mocks are set up
  const handler = await import('~/server/api/test.post')

  it('is registered as an event handler', () => expect(defineEventHandler).toHaveBeenCalled())

  it('return success response for valid request', async () => {
    const event = createMockH3Event({
      body: {
        // Simulate a valid request body
      },
    })

    const response = await handler.default(event)

    expect(response).toEqual({ data: 'mocked data' })
  })
})

5. Mocking Built-in Composables

In Nuxt 3, many server-side functionalities are encapsulated in composables. For example, useRuntimeConfig is commonly used to access environment variables or configuration settings.
To mock these composables, you can use mockNuxtImport from @nuxt/test-utils/runtime. This allows you to provide a mock implementation that your handler can use during tests.

// test/server/api/test.post.test.ts
import { vi } from 'vitest'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'

const { useRuntimeConfigMock } = vi.hoisted(() => ({
  useRuntimeConfigMock: vi.fn(() => ({
    gemini: {
      apiKey: 'test-api-key',
    },
  })),
}))
mockNuxtImport('useRuntimeConfig', () => useRuntimeConfigMock)

6. Mocking Custom Functions

If your server route relies on custom functions (like fetching data from an external API), you can mock these as well. This allows you to control the responses and test how your handler behaves under different scenarios.

// test/server/api/test.post.test.ts
import { vi } from 'vitest'

// Mock custom functions
const { formatDate } = vi.hoisted(() => ({
  formatDate: vi.fn(),
}))

vi.mock('~/server/utils/datetime', () => ({
  formatDate,
}))

describe('POST /api/test', () => {
  it('formats date correctly', async () => {
    formatDate.mockReturnValue('2023-10-01')

    const event = createMockH3Event({
      body: { date: '2023-10-01T00:00:00Z' },
    })

    const response = await handler.default(event)

    expect(response).toEqual({ formattedDate: '2023-10-01' })
    expect(formatDate).toHaveBeenCalledWith('2023-10-01T00:00:00Z')
  })
})

Breakdown of the Test File:

  1. Mock Composables: Use mockNuxtImport from @nuxt/test-utils/runtime to provide a mock implementation for server-side composables like useRuntimeConfig.
  2. Initialize Utilities: Call the test utility functions (useH3TestUtils, etc.) at the top level to set up the global mocks.
  3. Dynamic Import: Import your API handler inside the describe block. This is critical to ensure that all your mocks are in place before the handler module is loaded.
  4. Simulate Requests: Use createMockH3Event to craft the specific request context for each test case.
  5. Assert: Make assertions against the handler’s return value and check if your mocked functions were called with the expected arguments.

Follow-up Steps

In next article, we will explore how to:

  • Mock third-party libraries and services.

Conclusion

By combining a global setup file, a flexible event factory, and targeted mocking, you can build a comprehensive and maintainable test suite for your Nuxt 3 server routes. This setup not only isolates your handlers for true unit testing but also provides a clear and repeatable pattern for testing all your server-side logic.

Happy testing!

References


This content originally appeared on DEV Community and was authored by Doan Trong Nam