404 Not Found

404 Not Found


nginx

Testing Vitest + Playwright

MegaShop has gone live, but every release has had bugs—shopping cart calculation errors, occasional login failures, and 404 errors on product pages. Charlie decided to set up an automated testing system, using Vitest for unit tests and Playwright for end-to-end (E2E) tests, to ensure reliable quality with every release.

1. What You'll Learn


2. A True Story of an Architect

(1) Pain Point: Manual testing cannot cover all scenarios

Before every release, Bob manually tests MegaShop—the homepage, product pages, shopping cart, checkout, and admin panel. 5 browsers x 3 languages x 20 pages = 300 manual tests. With a 30% test coverage gap, at least 2 live bugs are found with every release.

(2) Solutions for Automated Testing

Vitest + Playwright: Automating Test Runs:

TYPESCRIPT
// Vitest: unit test for usePriceFormat
test('formats USD price correctly', () => {
  const price = ref(2999.99)
  const { formatted } = usePriceFormat(price)
  expect(formatted.value).toBe('$2,999.99')
})

(3) Results: 0 manual tests + 0 production bugs

Automated testing covers core processes; CI runs automatically with every commit, reducing the test omission rate to 2% and bringing the number of production bugs down to 0.


3. The Testing Pyramid

(1) Test Hierarchy

100%
graph TB
    A[E2E Tests - Playwright<br/>Critical user flows] --> B[Integration Tests<br/>API + Component interactions]
    B --> C[Unit Tests - Vitest<br/>Composables / Stores / Utils]
    
    style A fill:#f96,stroke:#333
    style B fill:#ff9,stroke:#333
    style C fill:#9f9,stroke:#333

(2) Comparison of Test Types

Dimension Unit Testing Integration Testing E2E Testing
Tools Vitest Vitest Playwright
Scope Single Function/Component Interaction Among Multiple Modules Complete User Flow
Speed ⚡⚡⚡ Fast ⚡⚡ Medium 🐢 Slow
Quantity High Medium Low
Value Logical Correctness Interface Consistency User Flow Assurance
MegaShop Example usePriceFormat API Routes Cart → Checkout

4. Vitest Configuration and Unit Testing

(1) ▶ Example: Installation and Configuration

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

Output:

TEXT
# Command executed successfully
TYPESCRIPT
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    globals: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      exclude: ['node_modules', '.nuxt']
    }
  }
})

(2) ▶ Example: Composable Unit Tests

TYPESCRIPT
// tests/composables/usePriceFormat.test.ts
import { ref } from 'vue'

describe('usePriceFormat', () => {
  test('formats USD price with comma separator', () => {
    const price = ref(2999.99)
    const { formatted } = usePriceFormat(price)
    expect(formatted.value).toBe('$2,999.99')
  })

  test('formats JPY price with no decimals', () => {
    const price = ref(29999)
    const { formatted } = usePriceFormat(price, { currency: 'JPY', locale: 'ja-JP' })
    expect(formatted.value).toBe('¥29,999')
  })

  test('reactively updates when price changes', () => {
    const price = ref(100)
    const { formatted } = usePriceFormat(price)
    expect(formatted.value).toBe('$100.00')
    price.value = 200
    expect(formatted.value).toBe('$200.00')
  })

  test('handles zero price', () => {
    const price = ref(0)
    const { formatted } = usePriceFormat(price)
    expect(formatted.value).toBe('$0.00')
  })

  test('handles raw number input (not ref)', () => {
    const { formatted } = usePriceFormat(50.5)
    expect(formatted.value).toBe('$50.50')
  })
})

Output:

TEXT
// Execution Successful

(3) ▶ Example: Pinia Store Test

TYPESCRIPT
// tests/stores/cart.test.ts
import { setActivePinia, createPinia } from 'pinia'

describe('useCartStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  test('adds item to cart', () => {
    const store = useCartStore()
    store.addItem({ id: 1, name: 'Headphones', price: 299.99, image: '/img.jpg' })
    expect(store.items.length).toBe(1)
    expect(store.totalItems).toBe(1)
  })

  test('increments quantity for existing item', () => {
    const store = useCartStore()
    const product = { id: 1, name: 'Headphones', price: 299.99, image: '/img.jpg' }
    store.addItem(product)
    store.addItem(product)
    expect(store.items.length).toBe(1)
    expect(store.items[0].quantity).toBe(2)
  })

  test('removes item from cart', () => {
    const store = useCartStore()
    store.addItem({ id: 1, name: 'Headphones', price: 299.99, image: '/img.jpg' })
    store.removeItem(1)
    expect(store.items.length).toBe(0)
  })

  test('calculates total price correctly', () => {
    const store = useCartStore()
    store.addItem({ id: 1, name: 'A', price: 100, image: '/a.jpg' })
    store.addItem({ id: 2, name: 'B', price: 200, image: '/b.jpg' })
    store.addItem({ id: 1, name: 'A', price: 100, image: '/a.jpg' }) // quantity = 2
    expect(store.totalPrice).toBe(400) // 100*2 + 200
  })

  test('clears cart', () => {
    const store = useCartStore()
    store.addItem({ id: 1, name: 'A', price: 100, image: '/a.jpg' })
    store.clearCart()
    expect(store.items.length).toBe(0)
    expect(store.totalPrice).toBe(0)
  })
})

Output:

TEXT
// Execution Successful

5. Component Testing

(1) ▶ Example: Testing the mountSuspended component

TYPESCRIPT
// tests/components/ProductCard.test.ts
import { mountSuspended } from '@nuxt/test-utils/runtime'

describe('ProductCard', () => {
  const mockProduct = {
    id: 1, name: 'Premium Headphones', price: 299.99,
    image: '/headphones.jpg', inStock: true
  }

  test('renders product name and price', async () => {
    const wrapper = await mountSuspended(ProductCard, {
      props: { product: mockProduct }
    })
    expect(wrapper.text()).toContain('Premium Headphones')
    expect(wrapper.text()).toContain('299.99')
  })

  test('emits add-to-cart event on button click', async () => {
    const wrapper = await mountSuspended(ProductCard, {
      props: { product: mockProduct }
    })
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('add-to-cart')).toBeTruthy()
    expect(wrapper.emitted('add-to-cart')[0][0]).toEqual(mockProduct)
  })

  test('shows out of stock when not available', async () => {
    const wrapper = await mountSuspended(ProductCard, {
      props: { product: { ...mockProduct, inStock: false } }
    })
    expect(wrapper.text()).toContain('Out of Stock')
  })
})

Output:

TEXT
// Execution Successful

6. Playwright E2E Testing

(1) ▶ Example: Playwright Configuration

TYPESCRIPT
// playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  baseURL: 'http://localhost:3000',
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI
  },
  use: {
    headless: true,
    screenshot: 'only-on-failure'
  }
})

Output:

TEXT
// Execution Successful

(2) ▶ Example: End-to-End Shopping Cart Workflow

TYPESCRIPT
// e2e/cart-flow.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Shopping Cart Flow', () => {
  test('add product to cart and verify', async ({ page }) => {
    // Navigate to product list
    await page.goto('/products')

    // Wait for products to load
    await page.waitForSelector('.product-card')

    // Click first product's "Add to Cart" button
    await page.locator('.product-card:first-child button').click()

    // Verify cart count updated
    const cartCount = page.locator('.cart-count')
    await expect(cartCount).toHaveText('1')

    // Navigate to cart page
    await page.goto('/cart')

    // Verify item in cart
    await expect(page.locator('.cart-item')).toHaveCount(1)
  })

  test('complete checkout flow', async ({ page }) => {
    // Login first
    await page.goto('/login')
    await page.fill('input[type="email"]', 'alice@example.com')
    await page.fill('input[type="password"]', 'password123')
    await page.click('button[type="submit"]')

    // Add item and go to checkout
    await page.goto('/products/1')
    await page.click('button:has-text("Add to Cart")')
    await page.click('a:has-text("Cart")')
    await page.click('a:has-text("Proceed to Checkout")')

    // Verify checkout page
    await expect(page.locator('h1')).toHaveText(/checkout/i)
  })
})

Output:

TEXT
// Execution Successful

7. API Testing

(1) ▶ Example: Server API Testing

TYPESCRIPT
// tests/api/products.test.ts
import { setupTest } from '@nuxt/test-utils'

describe('Products API', () => {
  setupTest()

  test('GET /api/products returns product list', async () => {
    const response = await $fetch('/api/products')
    expect(response.items).toBeDefined()
    expect(response.total).toBeGreaterThan(0)
    expect(response.page).toBe(1)
  })

  test('GET /api/products/:id returns product detail', async () => {
    const product = await $fetch('/api/products/1')
    expect(product.id).toBe(1)
    expect(product.name).toBeDefined()
    expect(product.price).toBeDefined()
  })

  test('GET /api/products/:id returns 404 for invalid id', async () => {
    await expect($fetch('/api/products/99999')).rejects.toThrow('404')
  })

  test('GET /api/products supports pagination', async () => {
    const page1 = await $fetch('/api/products?page=1&limit=5')
    const page2 = await $fetch('/api/products?page=2&limit=5')
    expect(page1.items.length).toBeLessThanOrEqual(5)
    expect(page2.items[0].id).not.toBe(page1.items[0].id)
  })

  test('GET /api/products supports category filter', async () => {
    const result = await $fetch('/api/products?category=electronics')
    result.items.forEach((item: any) => {
      expect(item.category).toBe('electronics')
    })
  })
})

Output:

TEXT
// Execution Successful

8. Comprehensive Example: The MegaShop Testing Framework

JSON
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "test:all": "npm run test && npm run test:e2e"
  }
}
Test Type Command Coverage When to Run
Unit Tests vitest Composable/Store/Utils Every commit
Component Testing vitest Vue Components On Every Commit
API Testing vitest Server API Each Submission
E2E Testing Playwright User Flow Before PR Merge
Coverage vitest --coverage All CI Pipeline

❓ FAQ

Q What is the difference between Vitest and Jest?
A Vitest is based on Vite and shares build configurations with Nuxt 3, resulting in faster startup times. Jest requires additional Babel/webpack configuration. Nuxt 3 recommends using Vitest.
Q What is the difference between mountSuspended and mount?
A mount is the basic mounting function provided by Vue Test Utils, while mountSuspended is provided by @nuxt/test-utils and supports the full context, including Nuxt plugins, auto-imports, and Composables.
Q What should I do if E2E testing is too slow?
A Test only the key user flows (shopping cart/checkout/login), not every single page. Component testing covers UI logic, while E2E testing covers end-to-end flows.
Q How do I test SSR behavior?
A Use the $fetch method from @nuxt/test-utils to directly request the server-rendered HTML and verify that the HTML contains the data. You can also use Playwright to disable JavaScript and inspect the page.
Q Does the Pinia Store test require a mock API?
A The $fetch action called within the Store needs to be mocked. Use vitest.mock to intercept $fetch, or extract the API call into a Composable to facilitate mocking.
Q How do Vitest and Playwright work together in CI?
A Vitest runs after linting (fast, 1-2 minutes), and Playwright runs after the build (slow, 5-10 minutes). Both run when a pull request is created; only Vitest runs when pushing to the main branch.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Install Vitest and @nuxt/test-utils, and write 5 unit tests for usePriceFormat.
  2. Advanced Exercise (Difficulty: ⭐⭐): Write a complete CRUD test for cartStore and a component test for ProductCard.
  3. Challenge (Difficulty: ⭐⭐⭐): Write a complete end-to-end (E2E) test for a shopping cart using Playwright: Browse products → Add to cart → View cart → Checkout

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏