Тестирование Nuxt
Тестирование Nuxt-приложений отличается от обычных Vue-компонентов из-за auto-imports, server routes, SSR и Nitro-рантайма. Без правильной инфраструктуры тесты ломаются при попытках вызвать useFetch или useRuntimeConfig вне Nuxt-контекста. В статье разберём настройку Vitest, тестирование компонентов, composables, server API, middleware и плагинов, а также наиболее частые ошибки.
Содержание
- Особенности тестирования Nuxt
- Конфигурация Vitest для Nuxt
- Тестирование компонентов
- Тестирование composables
- Тестирование server API routes
- Тестирование middleware
- Тестирование plugins
- Тестирование useAsyncData / useFetch
- Мокирование Nuxt API
- Тестирование NuxtLink
- Лучшие практики
- Антипаттерны
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Особенности тестирования Nuxt
Nuxt 3 — это метафреймворк поверх Vue 3. Он добавляет слои абстракции, которые требуют особого подхода к тестированию:
| Компонент Nuxt | Особенность тестирования |
|---|---|
| Auto-imports | Composables и компоненты импортируются автоматически — в тестах это нужно учитывать |
| Server routes | API-эндпоинты в server/api/ — тестируются через registerEndpoint или HTTP-запросы |
| Middleware | Код в middleware/ — чистые функции Request → Response |
| Plugins | Регистрируются при старте приложения — нужна интеграционная среда |
| useAsyncData / useFetch | Работают с контекстом Nuxt — требуют mountSuspended |
| SSR | Компоненты рендерятся на сервере — нужно дожидаться завершения асинхронных операций |
| Nuxt runtime config | useRuntimeConfig() читает переменные окружения — нужно мокировать |
Что НЕ нужно тестировать
- Фреймворк Nuxt — он уже протестирован командой Nuxt
- Внутренний механизм рендеринга — гидратация, автоимпорты, Nitro
- Конфигурацию Nuxt —
nuxt.config.tsне нуждается в unit-тестах
Что НУЖНО тестировать
- Бизнес-логику в composables и server routes
- Валидацию данных в API-эндпоинтах
- Поведение клиентских компонентов
- Побочные эффекты middleware и plugins
- Интеграцию между composables и UI
Конфигурация Vitest для Nuxt
Для Nuxt используется официальный модуль @nuxt/test-utils/module. Он автоматически настраивает Vitest для работы с Nuxt: auto-imports, .vue файлы, runtime config, серверную часть.
Установка
npm install -D @nuxt/test-utils vitest @vue/test-utils
Базовая конфигурация
// vitest.config.ts
import { defineVitestConfig } from "@nuxt/test-utils/config";
export default defineVitestConfig({
test: {
environment: "nuxt",
globals: true,
},
});
💡
environment: "nuxt"— специальное окружение от@nuxt/test-utils, которое эмулирует Nuxt-рантайм. Это отличается от обычногоjsdomдля Vue.
Конфигурация с явным rootDir
// vitest.config.ts
import { defineVitestConfig } from "@nuxt/test-utils/config";
export default defineVitestConfig({
test: {
environment: "nuxt",
environmentOptions: {
nuxt: {
rootDir: ".",
domEnvironment: "happy-dom",
},
},
globals: true,
},
});
Скрипты в package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:unit": "vitest run --testNamePattern unit"
}
}
Тестирование компонентов
Клиентские компоненты в Nuxt тестируются почти так же, как обычные Vue-компоненты. Но если компонент использует useAsyncData, useFetch или другие Nuxt-composables, нужен mountSuspended.
Базовый компонент
<!-- SearchBar.vue -->
<template>
<form @submit.prevent="handleSubmit">
<input v-model="query" placeholder="Search..." />
<button type="submit">Search</button>
</form>
</template>
<script setup>
import { ref } from "vue";
const query = ref("");
const emit = defineEmits(["search"]);
function handleSubmit() {
emit("search", query.value);
}
</script>
Тест
import { mount } from "@vue/test-utils";
import { describe, it, expect } from "vitest";
import SearchBar from "./SearchBar.vue";
describe("SearchBar", () => {
it("emits search event on submit", async () => {
const wrapper = mount(SearchBar);
await wrapper.find("input").setValue("nuxt");
await wrapper.find("form").trigger("submit");
expect(wrapper.emitted("search")).toHaveLength(1);
expect(wrapper.emitted("search")[0]).toEqual(["nuxt"]);
});
});
mountSuspended для SSR-компонентов
<!-- UserProfile.vue -->
<template>
<div>
<h1>{{ user?.name }}</h1>
<p>{{ user?.email }}</p>
</div>
</template>
<script setup>
const { data: user } = await useFetch("/api/user/1");
</script>
import { mountSuspended, registerEndpoint } from "@nuxt/test-utils";
import UserProfile from "./UserProfile.vue";
describe("UserProfile", () => {
registerEndpoint("/api/user/1", () => ({
id: 1,
name: "Alice",
email: "alice@example.com",
}));
it("renders user data", async () => {
const wrapper = await mountSuspended(UserProfile);
expect(wrapper.text()).toContain("Alice");
expect(wrapper.text()).toContain("alice@example.com");
});
});
💡
mountSuspendedждёт завершения всех асинхронных операций в компоненте (например,useFetch) перед возвратом wrapper. Это критично для SSR-компонентов.
Тестирование composables
Composables — сердце бизнес-логики Nuxt-приложений. Их можно тестировать двумя способами: напрямую через mountSuspended в тестовом компоненте или с помощью mockNuxtImport.
Через тестовый компонент
// composables/useCounter.js
export function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment };
}
import { mountSuspended } from "@nuxt/test-utils";
import { defineComponent } from "vue";
import { useCounter } from "./composables/useCounter";
it("useCounter increments", async () => {
const TestComponent = defineComponent({
setup() {
return useCounter();
},
template: "<button @click=\"increment\">{{ count }}</button>",
});
const wrapper = await mountSuspended(TestComponent);
expect(wrapper.text()).toBe("0");
await wrapper.find("button").trigger("click");
expect(wrapper.text()).toBe("1");
});
Мокирование Nuxt-composables
import { mockNuxtImport } from "@nuxt/test-utils";
mockNuxtImport("useRuntimeConfig", () => {
return () => ({
public: {
apiBaseUrl: "https://api.example.com",
},
});
});
it("uses runtime config", () => {
const config = useRuntimeConfig();
expect(config.public.apiBaseUrl).toBe("https://api.example.com");
});
Тестирование server API routes
Server routes в Nuxt — это обработчики на базе h3. Их можно тестировать через registerEndpoint или напрямую вызывая обработчик.
Server route
// server/api/users.get.ts
import { defineEventHandler } from "h3";
export default defineEventHandler(async () => {
return [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
});
Тест через registerEndpoint
import { registerEndpoint } from "@nuxt/test-utils";
import { $fetch } from "ofetch";
describe("Users API", () => {
registerEndpoint("/api/users", defineEventHandler(() => [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]));
it("returns list of users", async () => {
const users = await $fetch("/api/users");
expect(users).toHaveLength(2);
expect(users[0].name).toBe("Alice");
});
});
Тест обработчика напрямую
// server/api/users.post.ts
import { defineEventHandler, readBody } from "h3";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
if (!body.name) {
throw createError({ statusCode: 400, statusMessage: "Name is required" });
}
return { id: 1, name: body.name };
});
import usersPost from "./server/api/users.post";
import { createEvent } from "h3";
it("creates user with valid data", async () => {
const event = createEvent({});
event.node.req.body = { name: "Alice" };
const result = await usersPost(event);
expect(result).toEqual({ id: 1, name: "Alice" });
});
⚠️ Прямой вызов обработчика быстрее, но менее реалистичен: вы не проверяете маршрутизацию, middleware и парсинг тела запроса.
Тестирование middleware
Middleware в Nuxt — это функции, которые выполняются перед рендерингом страницы. Они принимают to и from маршруты и могут делать navigateTo или abortNavigation.
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const auth = useAuth();
if (!auth.isAuthenticated && to.path !== "/login") {
return navigateTo("/login");
}
});
Тест
import authMiddleware from "./middleware/auth";
import { mockNuxtImport } from "@nuxt/test-utils";
mockNuxtImport("useAuth", () => {
return () => ({
isAuthenticated: false,
});
});
it("redirects unauthenticated users to login", () => {
const to = { path: "/dashboard" };
const from = { path: "/" };
const result = authMiddleware(to, from);
expect(result).toEqual("/login");
});
💡
navigateToв тестовом окружении возвращает строку URL или объект маршрута. Это позволяет проверить логику редиректа без реального перехода.
Тестирование plugins
Plugins в Nuxt регистрируются при старте приложения и добавляют глобальные свойства, provide-значения или выполняют side-эффекты.
// plugins/toast.client.ts
export default defineNuxtPlugin(() => {
return {
provide: {
toast: {
success: (message: string) => console.log(`Success: ${message}`),
error: (message: string) => console.error(`Error: ${message}`),
},
},
};
});
Тест
import toastPlugin from "./plugins/toast.client";
it("provides toast methods", () => {
const nuxtApp = useNuxtApp();
const pluginResult = toastPlugin(nuxtApp);
expect(pluginResult.provide.toast.success).toBeTypeOf("function");
expect(pluginResult.provide.toast.error).toBeTypeOf("function");
});
💡 Для сложных plugins часто проще протестировать их через интеграционный тест с
mountSuspended, чем мокировать весьnuxtApp.
Тестирование useAsyncData / useFetch
useAsyncData и useFetch — основные инструменты загрузки данных в Nuxt. Они зависят от Nuxt-контекста, поэтому для их тестирования используется mountSuspended + registerEndpoint.
<!-- PostsPage.vue -->
<template>
<div>
<div v-if="pending">Loading...</div>
<ul v-else>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
<script setup>
const { data: posts, pending } = await useFetch("/api/posts");
</script>
import { mountSuspended, registerEndpoint } from "@nuxt/test-utils";
import PostsPage from "./PostsPage.vue";
describe("PostsPage", () => {
registerEndpoint("/api/posts", () => [
{ id: 1, title: "First post" },
{ id: 2, title: "Second post" },
]);
it("renders posts after loading", async () => {
const wrapper = await mountSuspended(PostsPage);
expect(wrapper.text()).toContain("First post");
expect(wrapper.text()).toContain("Second post");
expect(wrapper.text()).not.toContain("Loading...");
});
});
Ошибка при загрузке
it("shows error message", async () => {
registerEndpoint("/api/posts", () => {
throw createError({ statusCode: 500, statusMessage: "Server error" });
});
const wrapper = await mountSuspended(PostsPage);
expect(wrapper.text()).toContain("Error");
});
Мокирование Nuxt API
Nuxt предоставляет множество встроенных composables, которые не существуют вне Nuxt-рантайма. В тестах их нужно мокировать.
useRoute
import { mockNuxtImport } from "@nuxt/test-utils";
mockNuxtImport("useRoute", () => {
return () => ({
path: "/products",
params: { id: "1" },
query: { page: "1" },
});
});
it("reads route params", () => {
const route = useRoute();
expect(route.params.id).toBe("1");
});
useRuntimeConfig
mockNuxtImport("useRuntimeConfig", () => {
return () => ({
public: {
apiUrl: "https://api.example.com",
featureFlag: true,
},
private: {
secretKey: "test-secret",
},
});
});
navigateTo
vi.mock("#app/composables/router", () => ({
navigateTo: vi.fn((to) => to),
}));
useRouter
mockNuxtImport("useRouter", () => {
return () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
});
});
Тестирование NuxtLink
<NuxtLink> — компонент Nuxt для навигации. В тестах его можно заменить на обычный <a> через stubs, чтобы не зависеть от роутера.
it("renders link", () => {
const wrapper = mount(Navigation, {
global: {
stubs: {
NuxtLink: {
props: ["to"],
template: "<a :href=\"to\"><slot /></a>",
},
},
},
});
const link = wrapper.find("a");
expect(link.attributes("href")).toBe("/about");
});
Или глобально в vitest.config.ts:
// vitest.config.ts
export default defineVitestConfig({
test: {
environment: "nuxt",
environmentOptions: {
nuxt: {
mocks: {
NuxtLink: true,
},
},
},
},
});
Лучшие практики
1. Используйте mountSuspended для компонентов с useFetch
// ✅ Правильно
const wrapper = await mountSuspended(PostsPage);
// ❌ Обычный mount может не дождаться данных
const wrapper = mount(PostsPage);
2. Мокируйте API через registerEndpoint
// ✅ Реалистично и стабильно
registerEndpoint("/api/users", () => [{ id: 1, name: "Alice" }]);
// ❌ Мокирование useFetch хрупко
vi.mock("useFetch", () => ({ useFetch: vi.fn() }));
3. Тестируйте server routes как функции
// ✅ Быстро и изолированно
const result = await handler(event);
expect(result).toEqual({ ... });
// ❌ Запуск реального сервера медленно
const result = await fetch("http://localhost:3000/api/users");
4. Мокируйте runtime config
mockNuxtImport("useRuntimeConfig", () => {
return () => ({ public: { apiUrl: "http://localhost:3000" } });
});
5. Очищайте моки между тестами
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
});
Антипаттерны
1. Тестирование фреймворка
// ❌ Тестирует Nuxt
it("auto-imports work", () => {
expect(useState).toBeDefined();
});
// ✅ Тестирует бизнес-логику
it("increments counter", async () => {
const wrapper = await mountSuspended(Counter);
await wrapper.find("button").trigger("click");
expect(wrapper.text()).toContain("1");
});
2. Избыточное мокирование
// ❌ Мокирует всё
mockNuxtImport("useRoute", ...);
mockNuxtImport("useRouter", ...);
mockNuxtImport("useRuntimeConfig", ...);
mockNuxtImport("useHead", ...);
// ✅ Мокирует только то, что нужно
mockNuxtImport("useRuntimeConfig", () => ({ public: { apiUrl: "..." } }));
3. Игнорирование асинхронности SSR
// ❌ Не дожидается загрузки данных
it("renders posts", () => {
const wrapper = mount(PostsPage);
expect(wrapper.text()).toContain("First post");
});
// ✅ Использует mountSuspended
it("renders posts", async () => {
const wrapper = await mountSuspended(PostsPage);
expect(wrapper.text()).toContain("First post");
});
4. Тестирование server routes через HTTP
// ❌ Медленно и ненадёжно
const res = await fetch("http://localhost:3000/api/users");
// ✅ Быстро и изолированно
registerEndpoint("/api/users", () => [...]);
const users = await $fetch("/api/users");
5. Хардкод тестовых данных
// ❌ Дублирование
const user = { id: 1, name: "Alice", email: "alice@example.com", role: "user" };
// ✅ Фабрика
const createUser = (overrides) => ({
id: crypto.randomUUID(),
name: "Test User",
email: "test@example.com",
role: "user",
...overrides,
});
Ключевые тезисы для интервью
- Nuxt 3 добавляет слои абстракции (auto-imports, server routes, SSR), которые требуют
@nuxt/test-utilsсenvironment: "nuxt"для эмуляции Nuxt-рантайма. mountSuspendedдожидается завершения асинхронных операций компонента, что критично для SSR-компонентов сuseFetchилиuseAsyncData;registerEndpointпозволяет стабить server routes без запуска Nitro-сервера.- Composables тестируются через тестовый компонент с
mountSuspendedилиmockNuxtImport; server routes — черезregisterEndpointдля реалистичности или прямой вызов обработчика h3 для скорости. - Middleware — чистые функции (
to, from → navigateTo/abortNavigation), мокируемые черезmockNuxtImport; plugins проверяют provide-значения и side-эффекты, часто проще через интеграционный тест. - Встроенные Nuxt-composables (
useRoute,useRouter,useRuntimeConfig) мокируются черезmockNuxtImportилиvi.mock;NuxtLinkстабится черезglobal.stubsили глобальную моку. - Тестируйте бизнес-логику, а не фреймворк; избегайте избыточного мокирования, хардкода данных и тестирования самого Nuxt — это делает тесты хрупкими.
Заключение
Тестирование Nuxt 3 требует учёта server-side контекста, auto-imports и SSR. Основной инструментарий — @nuxt/test-utils с mountSuspended, registerEndpoint и mockNuxtImport. Для компонентов с асинхронными данными всегда используйте mountSuspended, а server routes тестируйте как функции h3. Мокируйте только то, что необходимо: runtime config, route и API-эндпоинты. Избегайте тестирования самого фреймворка и хардкода тестовых данных — это делает тесты хрупкими. Регулярная практика с этими паттернами поможет уверенно покрывать бизнес-логику Nuxt-приложений.