Skip to content

Nuxt Testing — Unit Tests, Component Tests, and E2E Tests with Vitest

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Nuxt Testing. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Nuxt testing — write unit tests for composables and utilities, component tests with Vue Test Utils, and end-to-end tests with Playwright using Vitest.

In this lesson, you'll understand how to set up a testing environment for Nuxt and write tests at every level of your application.

What You'll Learn

How to install and configure Vitest with @nuxt/test-utils, write unit tests for composables and utility functions, test Vue components with mounting and assertions, write end-to-end tests with Playwright, and run tests in CI pipelines.

Why It Matters

Automated tests catch regressions before they reach production, reduce manual QA time, and give you confidence to refactor code. A Nuxt app without tests becomes harder to maintain as it grows — every change risks breaking existing functionality.

Real-World Use

A Nuxt-based checkout system at an e-commerce company uses 300+ unit tests and 50+ E2E tests that run on every pull request, catching 95% of regressions before deployment and reducing production incidents by 80%.

flowchart LR
    A[Test Pyramid] --> B[Unit Tests]
    A --> C[Component Tests]
    A --> D[E2E Tests]
    B --> E[Composables]
    B --> F[Utilities]
    C --> G[Vue Components]
    C --> H[Composables in Context]
    D --> I[User Flows]
    D --> J[Full Page Rendering]
    style A fill:#00dc82,color:#fff

Setup

Install testing dependencies:

npm install --save-dev vitest @vue/test-utils @nuxt/test-utils jsdom
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./tests/setup.ts'],
    include: ['**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'json'],
      include: ['composables/**', 'utils/**', 'components/**']
    }
  },
  resolve: {
    alias: {
      '~': '/',
      '@': '/'
    }
  }
});
// tests/setup.ts
import { vi } from 'vitest';

// Mock Nuxt composables
vi.mock('nuxt/app', () => ({
  useRuntimeConfig: () => ({
    public: {
      apiBase: 'https://api.example.com'
    }
  }),
  useState: vi.fn((key, init) => {
    const state = ref(init?.());
    return state;
  })
}));

Testing Composables

Tests for a composable that formats currency:

// composables/useCurrency.ts
export function useCurrency() {
  function format(amount: number, currency: string = 'USD'): string {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency
    }).format(amount);
  }

  function parse(formatted: string): number {
    const cleaned = formatted.replace(/[$,€£¥]/g, '');
    return parseFloat(cleaned);
  }

  return { format, parse };
}
// tests/composables/useCurrency.test.ts
import { describe, it, expect } from 'vitest';
import { useCurrency } from '~/composables/useCurrency';

describe('useCurrency', () => {
  const { format, parse } = useCurrency();

  it('formats USD correctly', () => {
    expect(format(19.99)).toBe('$19.99');
  });

  it('formats EUR correctly', () => {
    expect(format(42.50, 'EUR')).toBe('€42.50');
  });

  it('parses formatted string back to number', () => {
    expect(parse('$1,234.56')).toBe(1234.56);
  });

  it('handles zero', () => {
    expect(format(0)).toBe('$0.00');
  });

  it('handles large numbers', () => {
    expect(format(1000000)).toBe('$1,000,000.00');
  });
});

Expected output: All five tests pass, confirming the currency composable handles edge cases correctly.

Testing Components

Test a button component:

<!-- components/PrimaryButton.vue -->
<template>
  <button
    :class="['primary-btn', { 'primary-btn--loading': loading }]"
    :disabled="loading || disabled"
    @click="$emit('click')"
  >
    <span v-if="loading" class="spinner" />
    <slot />
  </button>
</template>

<script setup>
defineProps({
  loading: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false }
});
defineEmits(['click']);
</script>
// tests/components/PrimaryButton.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import PrimaryButton from '~/components/PrimaryButton.vue';

describe('PrimaryButton', () => {
  it('renders slot content', () => {
    const wrapper = mount(PrimaryButton, {
      slots: { default: 'Submit' }
    });
    expect(wrapper.text()).toContain('Submit');
  });

  it('emits click event when clicked', async () => {
    const wrapper = mount(PrimaryButton, {
      slots: { default: 'Click' }
    });
    await wrapper.trigger('click');
    expect(wrapper.emitted('click')).toBeTruthy();
    expect(wrapper.emitted('click').length).toBe(1);
  });

  it('does not emit click when disabled', async () => {
    const wrapper = mount(PrimaryButton, {
      props: { disabled: true },
      slots: { default: 'Disabled' }
    });
    await wrapper.trigger('click');
    expect(wrapper.emitted('click')).toBeFalsy();
  });

  it('shows spinner when loading', () => {
    const wrapper = mount(PrimaryButton, {
      props: { loading: true },
      slots: { default: 'Loading' }
    });
    expect(wrapper.find('.spinner').exists()).toBe(true);
    expect(wrapper.find('.primary-btn--loading').exists()).toBe(true);
  });
});

Expected output: All component tests pass, verifying rendering, event emission, and prop-driven states.

Testing Pages with @nuxt/test-utils

Use @nuxt/test-utils for full-page integration tests:

// tests/pages/index.test.ts
import { describe, it, expect } from 'vitest';
import { setup, $fetch } from '@nuxt/test-utils/e2e';

describe('Homepage', async () => {
  await setup({
    rootDir: './',
    server: true,
    browser: false
  });

  it('renders the homepage', async () => {
    const html = await $fetch('/');
    expect(html).toContain('Welcome');
    expect(html).toContain('Get Started');
  });

  it('includes SEO meta tags', async () => {
    const html = await $fetch('/');
    expect(html).toContain('<title>');
    expect(html).toContain('meta name="description"');
  });

  it('loads without server errors', async () => {
    const response = await $fetch('/');
    expect(response).not.toContain('Internal Server Error');
  });
});

Expected output: Integration tests that render the actual Nuxt app and verify HTML output and SEO tags.

End-to-End Tests with Playwright

Set up Playwright for browser-based testing:

// tests/e2e/auth.spec.ts
import { test, expect } from '@playwright/test';

test('user can log in', async ({ page }) => {
  // Navigate to login page
  await page.goto('/login');

  // Fill credentials
  await page.fill('[name="email"]', 'user@example.com');
  await page.fill('[name="password"]', 'password123');

  // Submit form
  await page.click('button[type="submit"]');

  // Verify redirect to dashboard
  await expect(page).toHaveURL(/\/dashboard/);
  await expect(page.locator('h1')).toContainText('Welcome');
});

test('invalid credentials show error', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[name="email"]', 'wrong@email.com');
  await page.fill('[name="password"]', 'wrongpassword');
  await page.click('button[type="submit"]');

  // Verify error message
  await expect(page.locator('.error-message')).toBeVisible();
  await expect(page.locator('.error-message')).toContainText('Invalid credentials');
});

test('unauthenticated user is redirected', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page).toHaveURL(/\/login/);
});

Expected output: Playwright tests that simulate real user interactions in a browser, verifying authentication flows end-to-end.

Running Tests

# Run unit and component tests
npx vitest

# Run with coverage
npx vitest --coverage

# Run E2E tests
npx playwright test

# Run all tests
npx vitest && npx playwright test

Expected output: All tests pass with a summary showing passed/failed counts and coverage percentages.

Common Mistakes

  1. Not mocking Nuxt composables: Composables like useFetch, useState, and useRoute require mocking in unit tests. Without mocks, tests fail because the Nuxt runtime is not available.

  2. Testing implementation details instead of behavior: Test what the component does (renders content, emits events), not how it does it (internal state variables, method names). Behavioral tests survive Refactoring.

  3. Forgetting to clean up test environment: Test files that modify global state or mocks can affect other tests. Use afterEach or afterAll hooks to reset mocks and clean up mounted components.

  4. Not testing error states: Tests often cover the "happy path" but skip error and loading states. Test what happens when data fetching fails, when props are invalid, or when network is unavailable.

  5. Running slow tests without grouping: E2E tests take longer than unit tests. Group them separately and run unit tests on every commit, E2E tests only before deployment or on PR merges.

Practice Questions

  1. What is the difference between unit tests and component tests? Answer: Unit tests test individual functions or composables in isolation. Component tests mount Vue components and test rendering, events, and props in a simulated DOM environment.

  2. Why must you mock Nuxt composables in unit tests? Answer: Nuxt composables depend on the Nuxt runtime, which is not available in a standard Vitest environment. Mocks provide the expected return values without running Nuxt itself.

  3. What does @nuxt/test-utils provide for Integration Testing? Answer: It starts a Nuxt server in the test environment, allowing tests to make HTTP requests to pages and API routes, verifying actual HTML output and server responses.

  4. When should you use E2E tests instead of unit tests? Answer: Use E2E tests for critical user flows (login, checkout, registration) that involve multiple pages and interactions. Use unit tests for isolated logic that can be verified independently.

Challenge

Set up a complete testing infrastructure with: unit tests for three composables covering edge cases, component tests for three core components (button, form input, modal) including error states, an E2E test that completes a full user flow (register, login, create content, log out), CI configuration that runs unit tests on every push and E2E on PRs, and a coverage threshold of 80%.

Mini Project

Add a test suite to an existing Nuxt project with: unit tests for all utility functions and composables (minimum 90% coverage), component tests for shared UI components (button, card, modal, form input), page tests for the homepage and one detail page using @nuxt/test-utils, E2E tests for the primary user flow (registration to content creation), and a test:ci npm script that runs all tests and generates a coverage report.

FAQ

Can I use Jest instead of Vitest?

: Yes, but Vitest is recommended because it integrates better with Vite, runs faster, and has built-in TypeScript support. Jest requires additional configuration to work with Nuxt's module system.

How do I test composables that use `useFetch`?

: Mock useFetch in your test setup file to return controlled data. Alternatively, use @nuxt/test-utils which provides a real Nuxt environment where useFetch works naturally.

What is the best way to test async operations in components?

: Use flushPromises() from Vue Test Utils after triggering async operations. This waits for all pending promises to resolve before asserting on the updated DOM state.

How do I test `definePageMeta` in pages?

: definePageMeta is processed at build time and doesn't need direct testing. Test the resulting page behavior instead — verify meta tags render in the HTML output.

Should I test Nuxt modules?

: Module developers should test module registration and configuration. Application developers test the components and APIs that modules provide, not the modules themselves.

What's Next

Learn about Nuxt Environment Variables and Configuration to manage runtime configuration, environment variables, and per-environment settings in your Nuxt application.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro