404 Not Found

404 Not Found


nginx

Testes Vitest + Playwright

O MegaShop entrou no ar, mas cada release teve bugs — erros de cálculo no carrinho de compras, falhas ocasionais de login, e erros 404 nas páginas de produtos. Charlie decidiu configurar um sistema de testes automatizados, usando Vitest para testes unitários e Playwright para testes ponta a ponta (E2E), para garantir qualidade confiável a cada release.

1. O Que Você Vai Aprender


2. Uma História Real de um Arquiteto

(1) Ponto Crítico: Testes manuais não cobrem todos os cenários

Antes de cada release, Bob testa manualmente o MegaShop — página inicial, páginas de produtos, carrinho de compras, checkout e painel administrativo. 5 navegadores x 3 idiomas x 20 páginas = 300 testes manuais. Com uma lacuna de cobertura de teste de 30%, pelo menos 2 bugs em produção são encontrados a cada release.

(2) Solução com Testes Automatizados

Vitest + Playwright: Automatizando a Execução de Testes:

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

(3) Resultados: 0 testes manuais + 0 bugs em produção

Testes automatizados cobrem processos essenciais; CI executa automaticamente a cada commit, reduzindo a taxa de omissão de testes para 2% e trazendo o número de bugs em produção para 0.


3. A Pirâmide de Testes

(1) Hierarquia de Testes

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) Comparação de Tipos de Testes

Dimensão Teste Unitário Teste de Integração Teste E2E
Ferramentas Vitest Vitest Playwright
Escopo Função/Componente Único Interação Entre Múltiplos Módulos Fluxo Completo do Usuário
Velocidade ⚡⚡⚡ Rápido ⚡⚡ Médio 🐢 Lento
Quantidade Alto Médio Baixo
Valor Correção Lógica Consistência de Interface Garantia do Fluxo do Usuário
Exemplo MegaShop usePriceFormat API Routes Cart → Checkout

4. Configuração do Vitest e Testes Unitários

(1) ▶Exemplo: Instalação e Configuração

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

Saída:

TEXT
# Comando executado com sucesso
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) ▶Exemplo: Testes Unitários de Composable

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')
  })
})

Saída:

TEXT
// Execução Bem-sucedida

(3) ▶Exemplo: Teste da Pinia Store

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' }) // quantidade = 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)
  })
})

Saída:

TEXT
// Execução Bem-sucedida

5. Testes de Componentes

(1) ▶Exemplo: Testando o componente mountSuspended

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')
  })
})

Saída:

TEXT
// Execução Bem-sucedida

6. Testes E2E com Playwright

(1) ▶Exemplo: Configuração do Playwright

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'
  }
})

Saída:

TEXT
// Execução Bem-sucedida

(2) ▶Exemplo: Fluxo E2E do Carrinho de Compras

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 }) => {
    // Navegar para lista de produtos
    await page.goto('/products')

    // Esperar produtos carregarem
    await page.waitForSelector('.product-card')

    // Clicar no botão "Add to Cart" do primeiro produto
    await page.locator('.product-card:first-child button').click()

    // Verificar contagem do carrinho atualizada
    const cartCount = page.locator('.cart-count')
    await expect(cartCount).toHaveText('1')

    // Navegar para a página do carrinho
    await page.goto('/cart')

    // Verificar item no carrinho
    await expect(page.locator('.cart-item')).toHaveCount(1)
  })

  test('complete checkout flow', async ({ page }) => {
    // Login primeiro
    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"]')

    // Adicionar item e ir para 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")')

    // Verificar página de checkout
    await expect(page.locator('h1')).toHaveText(/checkout/i)
  })
})

Saída:

TEXT
// Execução Bem-sucedida

7. Testes de API

(1) ▶Exemplo: Teste de API do Servidor

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')
    })
  })
})

Saída:

TEXT
// Execução Bem-sucedida

8. Exemplo Completo: O Framework de Testes do MegaShop

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"
  }
}
Tipo de Teste Comando Cobertura Quando Executar
Testes Unitários vitest Composable/Store/Utils A cada commit
Testes de Componentes vitest Vue Components A cada commit
Testes de API vitest Server API A cada submissão
Testes E2E Playwright Fluxo do Usuário Antes de merge do PR
Cobertura vitest --coverage Todos Pipeline CI

❓Perguntas Frequentes

P Qual é a diferença entre Vitest e Jest?
R Vitest é baseado no Vite e compartilha configurações de build com o Nuxt 3, resultando em tempos de inicialização mais rápidos. Jest requer configuração adicional de Babel/webpack. O Nuxt 3 recomenda usar Vitest.
P Qual é a diferença entre mountSuspended e mount?
R mount é a função básica de montagem fornecida pelo Vue Test Utils, enquanto mountSuspended é fornecida por @nuxt/test-utils e suporta o contexto completo, incluindo plugins do Nuxt, auto-imports e Composables.
P O que devo fazer se os testes E2E forem muito lentos?
R Teste apenas os fluxos essenciais do usuário (carrinho de compras/checkout/login), não cada página individual. Testes de componentes cobrem lógica de UI, enquanto testes E2E cobrem fluxos ponta a ponta.
P Como testar o comportamento SSR?
R Use o método $fetch de @nuxt/test-utils para requisitar diretamente o HTML renderizado pelo servidor e verificar se o HTML contém os dados. Você também pode usar o Playwright para desabilitar JavaScript e inspecionar a página.
P O teste da Pinia Store requer uma API mock?
R A ação $fetch chamada dentro da Store precisa ser mockada. Use vitest.mock para interceptar $fetch, ou extraia a chamada de API em um Composable para facilitar o mock.
P Como Vitest e Playwright trabalham juntos no CI?
R Vitest executa após linting (rápido, 1-2 minutos), e Playwright executa após o build (lento, 5-10 minutos). Ambos executam quando um pull request é criado; apenas Vitest executa ao fazer push para a branch principal.

📖Resumo


📝Exercícios

  1. Exercício Básico (Dificuldade ⭐): Instale Vitest e @nuxt/test-utils, e escreva 5 testes unitários para usePriceFormat.
  2. Exercício Avançado (Dificuldade: ⭐⭐): Escreva um teste CRUD completo para cartStore e um teste de componente para ProductCard.
  3. Desafio (Dificuldade: ⭐⭐⭐): Escreva um teste E2E completo para um carrinho de compras usando Playwright: Navegar produtos → Adicionar ao carrinho → Ver carrinho → Checkout

---|

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%