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

Bài 19: Testing Vue & Nuxt

Vitest cho unit testing. Vue Test Utils, component testing. @nuxt/test-utils, Playwright E2E testing. MSW cho API mocking. Testing composables, Pinia stores.

💻 Lập trình — Bài 19 Bài 19: Testing Vue & Nuxt

Vue.js & Nuxt: Từ Cơ bản đến Nâng cao

Phần 6: Testing, Deployment & Production

xdev.asia

1. Thiết lập 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. Component Testing với 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. Testing Composables

// 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. Testing Pinia Store

// 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. API Mocking với MSW

// 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. E2E Testing với 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')
  })
})

Bài tiếp theo: Performance Optimization — Bundle analysis, code splitting, caching.