Chuyển đến nội dung chính

第 19 課:測驗 Vue 和 Nuxt

Vitest 用於單元測試。 Vue Test Utils,元件測試。 @nuxt/test-utils,Playwright E2E 測試。用於 API 模擬的 MSW。測試可組合項目、Pinia 商店。

💻 程式設計 — 第 19 課 第 19 課:測驗 Vue 和 Nuxt

Vue.js 和 Nuxt:從基礎到高級

第 6 部分:測試、部署和生產

亞洲開發網

1. 設定Vitest

npx nuxi module add @nuxt/test-utils
npm i -D vitest @vue/test-utils happy-dom msw playwright-core
// vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    globals: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      include: ['components/**', 'composables/**', 'stores/**'],
    },
  },
})

2. 使用 Vue Test Utils 進行元件測試

// tests/components/LoginForm.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
import LoginForm from '~/components/LoginForm.vue'

describe('LoginForm', () => {
  it('renders form fields', () => {
    const wrapper = mount(LoginForm)
    expect(wrapper.find('input[type="email"]').exists()).toBe(true)
    expect(wrapper.find('input[type="password"]').exists()).toBe(true)
  })

  it('validates required fields', async () => {
    const wrapper = mount(LoginForm)
    await wrapper.find('form').trigger('submit')
    expect(wrapper.text()).toContain('Email là bắt buộc')
  })

  it('emits login event with credentials', async () => {
    const wrapper = mount(LoginForm)
    await wrapper.find('input[type="email"]').setValue('[email protected]')
    await wrapper.find('input[type="password"]').setValue('password123')
    await wrapper.find('form').trigger('submit')

    expect(wrapper.emitted('login')?.[0]).toEqual([{
      email: '[email protected]',
      password: 'password123',
    }])
  })
})

3. 測試可組合項

// tests/composables/useCounter.test.ts
import { describe, it, expect } from 'vitest'
import { useCounter } from '~/composables/useCounter'

describe('useCounter', () => {
  it('initializes with default value', () => {
    const { count } = useCounter()
    expect(count.value).toBe(0)
  })

  it('increments and decrements', () => {
    const { count, increment, decrement } = useCounter(10)
    increment()
    expect(count.value).toBe(11)
    decrement()
    expect(count.value).toBe(10)
  })
})

4. 測試 Pinia 商店

// tests/stores/cart.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach } from 'vitest'
import { useCartStore } from '~/stores/cart'

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

  it('adds item to cart', () => {
    const store = useCartStore()
    store.addItem({ id: '1', name: 'Laptop', price: 999, quantity: 1 })
    expect(store.items).toHaveLength(1)
    expect(store.total).toBe(999)
  })

  it('increases quantity if item exists', () => {
    const store = useCartStore()
    store.addItem({ id: '1', name: 'Laptop', price: 999, quantity: 1 })
    store.addItem({ id: '1', name: 'Laptop', price: 999, quantity: 1 })
    expect(store.items).toHaveLength(1)
    expect(store.items[0].quantity).toBe(2)
    expect(store.total).toBe(1998)
  })
})

5. 使用 MSW 進行 API 模擬

// tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Nguyen Van A' },
      { id: 2, name: 'Tran Thi B' },
    ])
  }),
  http.post('/api/login', async ({ request }) => {
    const body = await request.json() as { email: string }
    if (body.email === '[email protected]') {
      return HttpResponse.json({ token: 'fake-jwt' })
    }
    return HttpResponse.json({ error: 'Invalid' }, { status: 401 })
  }),
]

// tests/setup.ts
import { setupServer } from 'msw/node'
import { handlers } from './mocks/handlers'

export const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

6.@nuxt/test-utils

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

describe('App', async () => {
  await setup({ browser: true })

  it('renders homepage', async () => {
    const html = await $fetch('/')
    expect(html).toContain('Trang chủ')
  })

  it('navigates to about page', async () => {
    const page = await createPage('/')
    await page.click('a[href="/about"]')
    await page.waitForURL('/about')
    expect(await page.textContent('h1')).toBe('Giới thiệu')
  })
})

7. 與 Playwright 進行端對端測試

// e2e/login.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Login Flow', () => {
  test('successful login redirects to dashboard', async ({ page }) => {
    await page.goto('/login')
    await page.fill('[data-testid="email"]', '[email protected]')
    await page.fill('[data-testid="password"]', 'secret')
    await page.click('[data-testid="submit"]')
    await expect(page).toHaveURL('/dashboard')
    await expect(page.locator('h1')).toContainText('Dashboard')
  })

  test('invalid credentials show error', async ({ page }) => {
    await page.goto('/login')
    await page.fill('[data-testid="email"]', '[email protected]')
    await page.fill('[data-testid="password"]', 'wrong')
    await page.click('[data-testid="submit"]')
    await expect(page.locator('.error')).toContainText('Sai thông tin')
  })
})

下一篇: 效能最佳化 — 套件分析、程式碼分割、快取。