Тестирование

Как тестировать приложение на Nuxt.

Nuxt поддерживает сквозные (E2E) и юнит-тесты через @nuxt/test-utils: это набор утилит и согласованной конфигурации, выровненный с тестами ядра Nuxt и с подходами авторов модулей в экосистеме.

Установка

@nuxt/test-utils подтягивает тестовые зависимости как опциональные peer-пакеты, чтобы вы сами выбирали версии. Например:

  • вы можете выбрать между happy-dom и jsdom как DOM-окружением для Nuxt
  • вы можете выбрать между vitest, cucumber, jest и playwright для сквозных тестов
  • playwright-core требуется только в том случае, если вы хотите использовать встроенные в браузер утилиты тестирования (и не используете @playwright/test в качестве средства запуска тестов)
npm i --save-dev @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core

Юнит-тестирование

Для кода, которому нужен контекст Nuxt, доступна отдельная среда выполнения в тестах. На данный момент она рассчитана на Vitest; поддержка других раннеров возможна через патчи и приветствуется.

Настройка

  1. По желанию добавьте @nuxt/test-utils/module в nuxt.config: появится интеграция Vitest в Nuxt DevTools и запуск модульных тестов из процесса разработки.
    export default defineNuxtConfig({
      modules: [
        '@nuxt/test-utils/module',
      ],
    })
    
  2. Создайте vitest.config.ts со следующим содержимым:
    import { defineConfig } from 'vitest/config'
    import { defineVitestProject } from '@nuxt/test-utils/config'
    
    export default defineConfig({
      test: {
        projects: [
          {
            test: {
              name: 'unit',
              include: ['test/unit/*.{test,spec}.ts'],
              environment: 'node',
            },
          },
          {
            test: {
              name: 'e2e',
              include: ['test/e2e/*.{test,spec}.ts'],
              environment: 'node',
            },
          },
          await defineVitestProject({
            test: {
              name: 'nuxt',
              include: ['test/nuxt/*.{test,spec}.ts'],
              environment: 'nuxt',
            },
          }),
        ],
      },
    })
    
Если в конфиг Vitest импортируется @nuxt/test-utils, в package.json нужно указать "type": "module" либо переименовать файл конфигурации Vitest.

например, vitest.config.m{ts,js}.

Для тестов можно задать переменные окружения в файле .env.test.

Использование среды выполнения Nuxt

С проектами Vitest можно точно задавать, в каком окружении выполняются какие тесты:

  • Юнит-тесты: обычные юнит-тесты кладите в test/unit/ — они идут в окружении Node для скорости
  • Тесты Nuxt: сценарии, которым нужна среда выполнения Nuxt, кладите в test/nuxt/ — они выполняются в окружении Nuxt

Альтернатива: упрощённая настройка

Если нужна более простая схема и все тесты должны идти в окружении Nuxt, используйте такую конфигурацию:

import { defineVitestConfig } from '@nuxt/test-utils/config'
import { fileURLToPath } from 'node:url'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    // you can optionally set Nuxt-specific environment options
    // environmentOptions: {
    //   nuxt: {
    //     rootDir: fileURLToPath(new URL('./playground', import.meta.url)),
    //     domEnvironment: 'happy-dom', // 'happy-dom' (default) or 'jsdom'
    //     overrides: {
    //       // other Nuxt config you want to pass
    //     }
    //   }
    // }
  },
})

Если по умолчанию задано environment: 'nuxt', для отдельных файлов можно отключить окружение Nuxt Vitest.

// @vitest-environment node
import { test } from 'vitest'

test('my test', () => {
  // ... test without Nuxt environment!
})
Такой подход не рекомендуется: получается гибридное окружение, в котором работают Vite-плагины Nuxt, но не инициализируются точка входа Nuxt и nuxtApp. Из этого часто следуют трудно отлаживаемые ошибки.

Организация тестов

При настройке с проектами Vitest тесты можно организовать так:

Directory structure
test/
├── e2e/
   └── ssr.test.ts
├── nuxt/
   ├── components.test.ts
   └── composables.test.ts
├── unit/
   └── utils.test.ts

Конечно, структура может быть любой, но разделять среду выполнения Nuxt и сквозные E2E-тесты Nuxt важно для стабильности.

Запуск тестов

С настройкой проектов можно запускать разные наборы:

# Run all tests
npx vitest

# Run only unit tests
npx vitest --project unit

# Run only Nuxt tests
npx vitest --project nuxt

# Run tests in watch mode
npx vitest --watch
При запуске в окружении Nuxt тесты выполняются в happy-dom или jsdom; перед прогоном поднимается глобальное приложение Nuxt (в том числе плагины и код из app.vue).Следите, чтобы не портить глобальное состояние между тестами (или явно сбрасывайте его).

🎭 Встроенные моки

@nuxt/test-utils подключает встроенные заглушки (моки) для типичных API в среде DOM.

intersectionObserver

По умолчанию true — подставляется пустая заглушка класса IntersectionObserver.

indexedDB

По умолчанию false; при true подключается fake-indexeddb для рабочей имитации IndexedDB.

Их можно настроить в разделе environmentOptions файла vitest.config.ts:

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

export default defineVitestConfig({
  test: {
    environmentOptions: {
      nuxt: {
        mock: {
          intersectionObserver: true,
          indexedDb: true,
        },
      },
    },
  },
})

🛠️ Вспомогательные API

@nuxt/test-utils экспортирует вспомогательные функции, которые упрощают тесты Nuxt-приложений.

mountSuspended

mountSuspended монтирует компонент Vue в контекст Nuxt: учитываются асинхронная инициализация и то, что задают плагины Nuxt (инъекции и т.п.).

Внутри mountSuspended оборачивает mount из @vue/test-utils; опции и примеры — в документации Vue Test Utils.

Например:

// @noErrors
import { expect, it } from 'vitest'
import type { Component } from 'vue'

declare module '#components' {
  export const SomeComponent: Component
}
// ---cut---
// tests/components/SomeComponents.nuxt.spec.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { SomeComponent } from '#components'

it('can mount some component', async () => {
  const component = await mountSuspended(SomeComponent)
  expect(component.text()).toMatchInlineSnapshot(
    '"This is an auto-imported component"',
  )
})
// @noErrors
import { expect, it } from 'vitest'
// ---cut---
// tests/components/SomeComponents.nuxt.spec.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'
import App from '~/app.vue'

// tests/App.nuxt.spec.ts
it('can also mount an app', async () => {
  const component = await mountSuspended(App, { route: '/test' })
  expect(component.html()).toMatchInlineSnapshot(`
      "<div>This is an auto-imported component</div>
      <div> I am a global component </div>
      <div>/</div>
      <a href="/test"> Test link </a>"
    `)
})

renderSuspended

renderSuspended отрисовывает компонент Vue в контексте Nuxt через @testing-library/vue: снова учитываются асинхронная подготовка и плагины Nuxt.

Сочетайте с привычными утилитами Testing Library (screen, fireEvent и др.). Пакет @testing-library/vue нужно добавить в проект отдельно.

Между тестами Testing Library может полагаться на глобальные символы Vitest — при необходимости включите их в конфигурации Vitest.

Переданный компонент будет отображен внутри <div id="test-wrapper"></div>.

Примеры:

// @noErrors
import { expect, it } from 'vitest'
import type { Component } from 'vue'

declare module '#components' {
  export const SomeComponent: Component
}
// ---cut---
// tests/components/SomeComponents.nuxt.spec.ts
import { renderSuspended } from '@nuxt/test-utils/runtime'
import { SomeComponent } from '#components'
import { screen } from '@testing-library/vue'

it('can render some component', async () => {
  await renderSuspended(SomeComponent)
  expect(screen.getByText('This is an auto-imported component')).toBeDefined()
})
// @noErrors
import { expect, it } from 'vitest'
// ---cut---
// tests/App.nuxt.spec.ts
import { renderSuspended } from '@nuxt/test-utils/runtime'
import App from '~/app.vue'

it('can also render an app', async () => {
  const html = await renderSuspended(App, { route: '/test' })
  expect(html).toMatchInlineSnapshot(`
    "<div id="test-wrapper">
      <div>This is an auto-imported component</div>
      <div> I am a global component </div>
      <div>Index page</div><a href="/test"> Test link </a>
    </div>"
  `)
})

mockNuxtImport

mockNuxtImport подменяет автоимпортируемую функцию Nuxt. Например, для мока useState:

import { mockNuxtImport } from '@nuxt/test-utils/runtime'

mockNuxtImport('useState', () => {
  return () => {
    return { value: 'mocked storage' }
  }
})

// your tests here

Тип мока можно задать явно для типобезопасности; в фабрику передаётся оригинальная реализация — это удобно для сложных сценариев.

test/nuxt/import.test.ts
import { mockNuxtImport } from '@nuxt/test-utils/runtime'

mockNuxtImport<typeof useState>('useState', (original) => {
  return (...args) => {
    return { ...original('some-key'), value: 'mocked state' }
  }
})

// or specify the target to mock
mockNuxtImport(useState, (original) => {
  return (...args) => {
    return { ...original('some-key'), value: 'mocked state' }
  }
})

// your tests here
mockNuxtImport для одного автоимпорта вызывается не больше одного раза на файл. На самом деле это макрос, который преобразуется в vi.mock, а vi.mock поднимается, как описано в документации Vitest.

Чтобы в разных тестах подставлять разные реализации одного автоимпорта, объявите моки через vi.hoisted и передайте их в mockNuxtImport. Между тестами меняйте реализацию через API мока; не забывайте сбрасывать моки до или после теста, чтобы состояние не протекало между запусками.

import { vi } from 'vitest'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'

const { useStateMock } = vi.hoisted(() => {
  return {
    useStateMock: vi.fn(() => {
      return { value: 'mocked storage' }
    }),
  }
})

mockNuxtImport('useState', () => {
  return useStateMock
})

// Then, inside a test
useStateMock.mockImplementation(() => {
  return { value: 'something else' }
})

Если нужно менять поведение только внутри теста, можно использовать такой приём:

import { beforeEach, vi } from 'vitest'
import { mockNuxtImport } from '@nuxt/test-utils/runtime'

mockNuxtImport(useRoute, original => vi.fn(original))

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

// Then, inside a test
const useRouteOriginal = vi.mocked(useRoute).getMockImplementation()!
vi.mocked(useRoute).mockImplementation(
  (...args) => ({ ...useRouteOriginal(...args), path: '/mocked' }),
)

mockComponent

mockComponent подменяет компонент в приложении Nuxt. Первый аргумент — имя в PascalCase или относительный путь к компоненту. Второй — фабрика, возвращающая заглушку-компонент.

Пример для MyComponent:

import { mockComponent } from '@nuxt/test-utils/runtime'

mockComponent('MyComponent', {
  props: {
    value: String,
  },
  setup (props) {
    // ...
  },
})

// relative path or alias also works
mockComponent('~/components/my-component.vue', () => {
  // or a factory function
  return defineComponent({
    setup (props) {
      // ...
    },
  })
})

// or you can use SFC for redirecting to a mock component
mockComponent('MyComponent', () => import('./MockComponent.vue'))

// your tests here

Примечание. В фабрике нельзя замыкать локальные переменные извне: код поднимается (hoisting). API Vue и прочее импортируйте внутри самой фабрики.

import { mockComponent } from '@nuxt/test-utils/runtime'

mockComponent('MyComponent', async () => {
  const { ref, h } = await import('vue')

  return defineComponent({
    setup (props) {
      const counter = ref(0)
      return () => h('div', null, counter.value)
    },
  })
})

registerEndpoint

registerEndpoint регистрирует в Nitro маршрут с ответом-заглушкой — удобно, когда компонент тянет данные с API.

Первый аргумент — путь эндпоинта (например, /test/). Второй — фабрика, возвращающая тело ответа.

Имитация GET /test/:

import { registerEndpoint } from '@nuxt/test-utils/runtime'

registerEndpoint('/test/', () => ({
  test: 'test-field',
}))

По умолчанию используется метод GET. Другой метод задаётся объектом вторым аргументом вместо функции.

import { registerEndpoint } from '@nuxt/test-utils/runtime'

registerEndpoint('/test/', {
  method: 'POST',
  handler: () => ({ test: 'test-field' }),
})

У объекта допускаются поля:

  • handler: обработчик события
  • method: (необязательно) HTTP-метод (например 'GET', 'POST')
  • once: (необязательно) если true, обработчик сработает только для первого подходящего запроса и затем будет удалён

Примечание. Если запросы из компонента идут на внешний API, задайте baseURL и обнулите его через переопределения среды в конфигурации Nuxt ($test), чтобы запросы шли на сервер Nitro.

Конфликт со сквозным (E2E) тестированием

@nuxt/test-utils/runtime и @nuxt/test-utils/e2e рассчитаны на разные среды выполнения и не смешиваются в одном файле.

Нужны и юнит-хелперы, и E2E — разнесите сценарии по файлам: для runtime либо комментарий // @vitest-environment nuxt, либо суффикс .nuxt.spec.ts в имени файла.

app.nuxt.spec.ts

import { mockNuxtImport } from '@nuxt/test-utils/runtime'

mockNuxtImport('useState', () => {
  return () => {
    return { value: 'mocked storage' }
  }
})

app.e2e.spec.ts

import { $fetch, setup } from '@nuxt/test-utils/e2e'

await setup({
  setupTimeout: 10000,
})

// ...

Использование @vue/test-utils

Если нужны только @vue/test-utils без среды Nuxt (компоненты без композаблов Nuxt, автоимпорта и общего контекста), настройте Vitest так:

  1. Установите необходимые зависимости
    npm i --save-dev vitest @vue/test-utils happy-dom @vitejs/plugin-vue
    
  2. Создайте vitest.config.ts со следующим содержимым:
    import { defineConfig } from 'vitest/config'
    import vue from '@vitejs/plugin-vue'
    
    export default defineConfig({
      plugins: [vue()],
      test: {
        environment: 'happy-dom',
      },
    })
    
  3. Добавьте в package.json скрипт запуска тестов
    "scripts": {
      "build": "nuxt build",
      "dev": "nuxt dev",
      ...
      "test": "vitest"
    },
    
  4. Создайте простой компонент <HelloWorld> components/HelloWorld.vue со следующим содержимым:
    <template>
      <p>Hello world</p>
    </template>
    
  5. Создайте простой модульный тест для этого компонента. ~/components/HelloWorld.spec.ts
    import { describe, expect, it } from 'vitest'
    import { mount } from '@vue/test-utils'
    
    import HelloWorld from './HelloWorld.vue'
    
    describe('HelloWorld', () => {
      it('component renders Hello world properly', () => {
        const wrapper = mount(HelloWorld)
        expect(wrapper.text()).toContain('Hello world')
      })
    })
    
  6. Запустите Vitest
    npm run test
    

После этого можно писать юнит-тесты с @vue/test-utils в проекте на Nuxt без полной среды Nuxt в Vitest.

Сквозное (E2E) тестирование

Для сквозных тестов поддерживаются раннеры Vitest, Jest, Cucumber и Playwright.

Настройка

В каждом describe, где вызываются хелперы @nuxt/test-utils/e2e, сначала нужно инициализировать контекст через setup.

test/my-test.spec.ts
import { describe, test } from 'vitest'
import { $fetch, setup } from '@nuxt/test-utils/e2e'

describe('My test', async () => {
  await setup({
    // test context options
  })

  test('my test', () => {
    // ...
  })
})

setup регистрирует хуки beforeAll / beforeEach / afterEach / afterAll и поднимает тестовое окружение Nuxt.

Параметры setup:

Конфигурация Nuxt

  • rootDir: Путь к каталогу с приложением Nuxt, которое будет протестировано.
    • Тип: string
    • По умолчанию: '.'
  • configFile: Имя файла конфигурации.
    • Тип: string
    • По умолчанию: 'nuxt.config'

Тайминги

  • setupTimeout: лимит времени (мс) на инициализацию (сборка/генерация артефактов Nuxt — в зависимости от опций).
    • Тип: number
    • По умолчанию: 60000

Возможности

  • build: Следует ли запускать отдельный этап сборки.
    • Тип: boolean
    • По умолчанию: true (false, если browser или server отключены, или если указан host)
  • server: Следует ли запускать сервер для ответа на запросы в наборе тестов.
    • Тип: boolean
    • По умолчанию: true (false, если указан host)
  • port: если задан, порт поднимаемого тестового сервера будет равен этому значению.
    • Тип: number | undefined
    • По умолчанию: undefined
  • host: если задан, тесты идут против этого базового URL вместо подъёма отдельного сервера. Удобно для «живых» E2E против уже развёрнутого приложения или локально запущенного сервера — часто быстрее, чем каждый раз пересобирать окружение. См. пример ниже.
    • Тип: string
    • По умолчанию: undefined
  • browser: для тестов в браузере @nuxt/test-utils использует Playwright. При true браузер запускается и доступен в рамках набора тестов.
    • Тип: boolean
    • По умолчанию: false
  • browserOptions
    • Тип: объект со следующими полями
      • type: тип браузера — Chromium, Firefox или WebKit
      • launch: объект опций, передаваемых Playwright при запуске; см. справочник API
  • runner: тестовый раннер для набора. Рекомендуется Vitest.
    • Тип: 'vitest' | 'jest' | 'cucumber'
    • По умолчанию: 'vitest'
Пример сквозного теста целевого host

Типичный сценарий — гонять E2E против уже развёрнутого приложения (как в продакшене).

Для локальной разработки и CI часто выгоднее держать отдельный сервер и подключаться к нему: так обычно быстрее, чем каждый раз заново собирать тестовое окружение.

Передайте в setup свойство host с нужным базовым URL.

import { createPage, setup } from '@nuxt/test-utils/e2e'
import { describe, expect, it } from 'vitest'

describe('login page', async () => {
  await setup({
    host: 'http://localhost:8787',
  })

  it('displays the email and password fields', async () => {
    const page = await createPage('/login')
    expect(await page.getByTestId('email').isVisible()).toBe(true)
    expect(await page.getByTestId('password').isVisible()).toBe(true)
  })
})

API

$fetch(url)

Возвращает HTML страницы, сформированной на сервере.

import { $fetch } from '@nuxt/test-utils/e2e'

const html = await $fetch('/')

fetch(url)

Возвращает объект ответа серверного рендеринга страницы.

import { fetch } from '@nuxt/test-utils/e2e'

const res = await fetch('/')
const { body, headers } = res

url(path)

Собирает полный URL страницы (с портом тестового сервера).

import { url } from '@nuxt/test-utils/e2e'

const pageUrl = url('/page')
// 'http://localhost:6840/page'

Тестирование в браузере

Playwright в @nuxt/test-utils можно использовать из кода тестов или через штатный раннер Playwright.

createPage(url)

В Vitest, Jest или Cucumber createPage открывает страницу Playwright на уже поднятом тестовом сервере (путь — относительно его базового URL). Полный API страницы — в документации Playwright.

import { createPage } from '@nuxt/test-utils/e2e'

const page = await createPage('/page')
// you can access all the Playwright APIs from the `page` variable

Тестирование с помощью тест-раннера Playwright

Отдельно поддерживается сценарий с штатным раннером Playwright.

npm i --save-dev @playwright/test @nuxt/test-utils

Глобальные параметры Nuxt в playwright.config.ts совпадают с опциями setup() из этого раздела.

playwright.config.ts
import { fileURLToPath } from 'node:url'
import { defineConfig, devices } from '@playwright/test'
import type { ConfigOptions } from '@nuxt/test-utils/playwright'

export default defineConfig<ConfigOptions>({
  use: {
    nuxt: {
      rootDir: fileURLToPath(new URL('.', import.meta.url)),
    },
  },
  // ...
})
Узнать больше Посмотреть полный пример конфигурации.

В тестах импортируйте expect и test из @nuxt/test-utils/playwright:

tests/example.test.ts
import { expect, test } from '@nuxt/test-utils/playwright'

test('test', async ({ page, goto }) => {
  await goto('/', { waitUntil: 'hydration' })
  await expect(page.getByRole('heading')).toHaveText('Welcome to Playwright!')
})

Опции Nuxt можно задать и в самом файле теста через test.use:

tests/example.test.ts
import { expect, test } from '@nuxt/test-utils/playwright'

test.use({
  nuxt: {
    rootDir: fileURLToPath(new URL('..', import.meta.url)),
  },
})

test('test', async ({ page, goto }) => {
  await goto('/', { waitUntil: 'hydration' })
  await expect(page.getByRole('heading')).toHaveText('Welcome to Playwright!')
})