Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Тестирование Nuxt приложений
nuxt_testing

Тестирование Nuxt приложений

Vitest с Nuxt, @nuxt/test-utils, тестирование страниц, server routes и composables в Nuxt-окружении

Тестирование Nuxt приложений

Тестирование Nuxt отличается от обычного Vue: авто-импорты, server routes, middleware, SSR-специфичный код — всё это требует специальной настройки. @nuxt/test-utils решает большинство этих проблем.

#Стратегия тестирования: что и чем

Четыре уровня для Nuxt приложения:

Unit-тесты — composables, утилиты, Pinia stores. Не требуют Nuxt-контекста, самые быстрые. Запускаются в jsdom.

Component-тесты — Vue компоненты с Nuxt-специфичными зависимостями (useRoute, useState, useFetch). Требуют Nuxt среды для авто-импортов.

Integration-тесты server routes — тестирование API эндпоинтов: запросы, ответы, авторизация. Запускают реальный Nitro сервер.

E2E-тесты — полный сценарий в реальном браузере. Playwright + @nuxt/test-utils/e2e. Медленные, тестируют финальный продукт.

#Установка @nuxt/test-utils

npm install -D @nuxt/test-utils vitest @vue/test-utils happy-dom playwright-core

Конфигурация vitest.config.ts — обязательно используйте defineVitestConfig:

import { defineVitestConfig } from '@nuxt/test-utils/config' export default defineVitestConfig({ test: { environment: 'nuxt', // Nuxt-окружение для авто-импортов environmentOptions: { nuxt: { rootDir: '.' // Корень проекта } }, globals: true, coverage: { provider: 'v8', reporter: ['text', 'html'], exclude: ['**/*.config.*', 'tests/**'] } } })

Разница с обычным Vitest: environment: 'nuxt' разрешает авто-импорты и создаёт Nuxt-контекст. Без этого useRoute(), useState(), useFetch() бросят ошибки.

#Тестирование Nuxt компонентов: mountSuspense

Компоненты с async setup (использующие await useFetch()) нельзя монтировать обычным mount. Используйте mountSuspense:

<!-- components/UserCard.vue --> <script setup lang="ts"> const props = defineProps<{ userId: number }>() // async setup — требует Suspense обёртки const { data: user } = await useFetch(`/api/users/${props.userId}`) </script> <template> <div class="card" v-if="user"> <h2>{{ user.name }}</h2> <p>{{ user.email }}</p> </div> </template>
// components/UserCard.test.ts import { describe, it, expect, vi } from 'vitest' import { mountSuspense } from '@nuxt/test-utils/runtime' import UserCard from './UserCard.vue' // Мокируем useFetch vi.mock('#imports', async () => { const actual = await vi.importActual('#imports') return { ...actual, useFetch: vi.fn() } }) describe('UserCard', () => { it('отображает данные пользователя', async () => { vi.mocked(useFetch).mockResolvedValueOnce({ data: ref({ name: 'Анна Иванова', email: 'anna@example.com' }), pending: ref(false), error: ref(null) } as any) const wrapper = await mountSuspense(UserCard, { props: { userId: 1 } }) expect(wrapper.find('h2').text()).toBe('Анна Иванова') expect(wrapper.find('p').text()).toBe('anna@example.com') }) })

#mockNuxtImport: мокирование авто-импортов

mockNuxtImport — специальный helper для подмены авто-импортируемых composables:

import { mockNuxtImport } from '@nuxt/test-utils/runtime' import { mountSuspense } from '@nuxt/test-utils/runtime' // Мокируем useUserSession из nuxt-auth-utils const { mockSession, restoreSession } = mockNuxtImport('useUserSession', () => { const user = ref<{ name: string; role: string } | null>(null) const loggedIn = computed(() => user.value !== null) function setUser(userData: { name: string; role: string } | null) { user.value = userData } return { user, loggedIn, setUser } }) describe('Navbar', () => { afterEach(() => { restoreSession() }) it('показывает кнопку входа для гостей', async () => { const wrapper = await mountSuspense(Navbar) expect(wrapper.find('[data-test="login-btn"]').exists()).toBe(true) }) it('показывает имя для авторизованных', async () => { mockSession({ name: 'Иван', role: 'user' }) const wrapper = await mountSuspense(Navbar) expect(wrapper.text()).toContain('Иван') }) })

#Тестирование composables с Nuxt контекстом

Composables, использующие Nuxt-специфичные функции, нужно тестировать в Nuxt-окружении:

// composables/useCart.ts export function useCart() { // useState — Nuxt composable для SSR-совместимого состояния const cartItems = useState<CartItem[]>('cart', () => []) function addItem(item: CartItem) { const existing = cartItems.value.find(i => i.id === item.id) if (existing) { existing.quantity++ } else { cartItems.value.push({ ...item, quantity: 1 }) } } const totalPrice = computed(() => cartItems.value.reduce((sum, i) => sum + i.price * i.quantity, 0) ) return { cartItems, addItem, totalPrice } }
// composables/useCart.test.ts import { describe, it, expect, beforeEach } from 'vitest' describe('useCart', () => { beforeEach(() => { // Сбрасываем useState между тестами const nuxtApp = useNuxtApp() nuxtApp.payload.state['cart'] = [] }) it('добавляет новый товар', () => { const { cartItems, addItem } = useCart() addItem({ id: 1, name: 'Книга', price: 500 }) expect(cartItems.value).toHaveLength(1) expect(cartItems.value[0].quantity).toBe(1) }) it('увеличивает количество существующего товара', () => { const { cartItems, addItem } = useCart() addItem({ id: 1, name: 'Книга', price: 500 }) addItem({ id: 1, name: 'Книга', price: 500 }) expect(cartItems.value).toHaveLength(1) expect(cartItems.value[0].quantity).toBe(2) }) it('считает общую стоимость', () => { const { addItem, totalPrice } = useCart() addItem({ id: 1, name: 'Книга', price: 500 }) addItem({ id: 2, name: 'Курс', price: 2000 }) expect(totalPrice.value).toBe(2500) }) })

#Тестирование server routes

Server routes тестируются через $fetch в integration тестах. setup() запускает Nuxt приложение:

// server/api/posts.get.ts export default defineEventHandler(async (event) => { const query = getQuery(event) const page = Number(query.page) || 1 const limit = Number(query.limit) || 10 const posts = await db.posts.findMany({ skip: (page - 1) * limit, take: limit }) return { posts, page, limit, total: await db.posts.count() } })
// tests/api/posts.test.ts import { describe, it, expect } from 'vitest' import { setup, $fetch } from '@nuxt/test-utils/e2e' // setup запускает Nuxt сервер (медленно — только для integration тестов) await setup({ rootDir: '.', server: true }) describe('GET /api/posts', () => { it('возвращает список постов', async () => { const response = await $fetch('/api/posts') expect(response.posts).toBeInstanceOf(Array) expect(response.page).toBe(1) expect(response.limit).toBe(10) }) it('поддерживает пагинацию', async () => { const response = await $fetch('/api/posts?page=2&limit=5') expect(response.page).toBe(2) expect(response.limit).toBe(5) expect(response.posts).toHaveLength(5) }) it('возвращает 401 для защищённых маршрутов', async () => { // Нет авторизации → 401 await expect($fetch('/api/admin/stats')).rejects.toMatchObject({ status: 401 }) }) })

#Тестирование Pinia в Nuxt контексте

В Nuxt среде Pinia инициализируется автоматически. Важно очищать состояние между тестами:

import { describe, it, expect, beforeEach } from 'vitest' import { useAuthStore } from '@/stores/auth' describe('useAuthStore', () => { let authStore: ReturnType<typeof useAuthStore> beforeEach(() => { // Создаём свежий инстанс Pinia const pinia = createPinia() setActivePinia(pinia) authStore = useAuthStore() }) it('начинает с неавторизованного состояния', () => { expect(authStore.isLoggedIn).toBe(false) expect(authStore.user).toBeNull() }) it('устанавливает пользователя после логина', async () => { // Мокируем API vi.mocked($fetch).mockResolvedValueOnce({ user: { id: 1, name: 'Тест', role: 'user' } }) await authStore.login('test@example.com', 'password') expect(authStore.isLoggedIn).toBe(true) expect(authStore.user?.name).toBe('Тест') }) })

#E2E тесты с Playwright

Для E2E нужен запущенный Nuxt сервер. @nuxt/test-utils/e2e запускает его автоматически:

// tests/e2e/auth.spec.ts import { describe, it, expect, beforeAll } from 'vitest' import { setup, createPage, url } from '@nuxt/test-utils/e2e' // Запустить Nuxt сервер await setup({ rootDir: '.', browser: true // Включает поддержку браузера через Playwright }) describe('Авторизация', () => { it('пользователь может войти', async () => { const page = await createPage() await page.goto(url('/login')) await page.fill('[data-test="email"]', 'user@example.com') await page.fill('[data-test="password"]', 'password123') await page.click('[data-test="submit"]') // Ждём редиректа await page.waitForURL(url('/dashboard')) const userName = await page.locator('[data-test="user-name"]').textContent() expect(userName).toContain('Иван') }) it('блокирует доступ к защищённым страницам', async () => { const page = await createPage() await page.goto(url('/dashboard')) // Должен редиректнуть на логин await page.waitForURL(/\/login/) expect(page.url()).toContain('/login') }) })

#Тестирование SSR: проверка HTML от сервера

Важный класс тестов — убедиться что SSR-рендеринг работает корректно:

import { $fetch } from '@nuxt/test-utils/e2e' it('страница рендерится на сервере с контентом', async () => { // $fetch без javascript — получаем чистый HTML от сервера const html = await $fetch('/', { responseType: 'text' }) // Проверяем что ключевой контент присутствует в HTML (не добавлен JS) expect(html).toContain('<title>') expect(html).toContain('data-server-rendered="true"') // Проверяем что мета-теги сгенерированы expect(html).toContain('meta name="description"') })

#CI/CD: GitHub Actions

Настройка автоматического запуска тестов в GitHub Actions:

# .github/workflows/test.yml name: Tests on: push: branches: [main, develop] pull_request: branches: [main] jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - run: npm ci - run: npm run test:run - run: npm run test:coverage e2e-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - run: npm ci - run: npx playwright install --with-deps chromium - run: npm run build - run: npx playwright test - uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report path: playwright-report/

#Типичные ловушки

Ошибка 1: window is not defined в server-side коде. Nuxt выполняет код и на сервере, где нет window. Оборачивайте browser-only код в if (import.meta.client) или onMounted.

Ошибка 2: Nuxt context outside setup. Composables, использующие useNuxtApp(), нельзя вызывать вне компонента или setup функции. В тестах убедитесь что вы в правильном контексте.

Ошибка 3: авто-импорты не работают. Если тесты падают с "useRoute is not defined" — вы не используете environment: 'nuxt' в vitest.config.ts.

Ошибка 4: тестирование деталей реализации вместо поведения. Не проверяйте внутренние переменные компонента. Проверяйте что пользователь видит и что происходит после его действий.

Далее: Производительность и оптимизация