Testes Automatizados Frontend: Guia Completo 2025

January 15, 2025 (1y ago)

Fala dev! 👋

Testes automatizados não são mais opcionais no desenvolvimento frontend moderno. São uma ferramenta essencial que previne bugs, facilita refatoração e garante a qualidade do código.

Neste guia completo, vou te mostrar como implementar testes robustos e eficientes para aplicações frontend, com as melhores práticas e ferramentas de 2025.


🎯 Por que testes automatizados?

Benefícios reais:

Estatísticas impressionantes:


🛠️ Ferramentas essenciais

1. Vitest (Recomendado)

# Instalar Vitest
pnpm add -D vitest @vitejs/plugin-react jsdom
 
# Configurar vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
 
export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true,
  },
})

2. Testing Library

# Instalar Testing Library
pnpm add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
// src/test/setup.ts
import '@testing-library/jest-dom'
import { expect, afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
 
afterEach(() => {
  cleanup()
})

3. Jest (Alternativa)

# Instalar Jest
pnpm add -D jest @types/jest ts-jest @testing-library/jest-dom
 
# Configurar jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
  moduleNameMapping: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
}

🧪 Tipos de testes

1. Unit Tests - Testes unitários

// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
 
describe('Button', () => {
  it('deve renderizar o texto correto', () => {
    render(<Button>Clique aqui</Button>)
    expect(screen.getByText('Clique aqui')).toBeInTheDocument()
  })
 
  it('deve chamar onClick quando clicado', () => {
    const handleClick = vi.fn()
    render(<Button onClick={handleClick}>Clique aqui</Button>)
    
    fireEvent.click(screen.getByText('Clique aqui'))
    expect(handleClick).toHaveBeenCalledTimes(1)
  })
 
  it('deve estar desabilitado quando disabled', () => {
    render(<Button disabled>Clique aqui</Button>)
    expect(screen.getByRole('button')).toBeDisabled()
  })
 
  it('deve aplicar variantes corretamente', () => {
    render(<Button variant="primary">Primário</Button>)
    expect(screen.getByRole('button')).toHaveClass('bg-blue-500')
  })
})

2. Integration Tests - Testes de integração

// components/UserForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { UserForm } from './UserForm'
 
describe('UserForm', () => {
  it('deve submeter formulário com dados válidos', async () => {
    const user = userEvent.setup()
    const onSubmit = vi.fn()
    
    render(<UserForm onSubmit={onSubmit} />)
    
    await user.type(screen.getByLabelText('Nome'), 'João Silva')
    await user.type(screen.getByLabelText('Email'), 'joao@email.com')
    await user.click(screen.getByRole('button', { name: 'Salvar' }))
    
    await waitFor(() => {
      expect(onSubmit).toHaveBeenCalledWith({
        name: 'João Silva',
        email: 'joao@email.com'
      })
    })
  })
 
  it('deve mostrar erros de validação', async () => {
    const user = userEvent.setup()
    render(<UserForm onSubmit={vi.fn()} />)
    
    await user.click(screen.getByRole('button', { name: 'Salvar' }))
    
    expect(screen.getByText('Nome é obrigatório')).toBeInTheDocument()
    expect(screen.getByText('Email é obrigatório')).toBeInTheDocument()
  })
})

3. E2E Tests - Testes end-to-end

// e2e/user-flow.spec.ts
import { test, expect } from '@playwright/test'
 
test('fluxo completo de usuário', async ({ page }) => {
  // Navegar para a página
  await page.goto('/')
  
  // Verificar se a página carregou
  await expect(page).toHaveTitle('Minha App')
  
  // Clicar em "Criar usuário"
  await page.click('text=Criar usuário')
  
  // Preencher formulário
  await page.fill('[data-testid="name-input"]', 'João Silva')
  await page.fill('[data-testid="email-input"]', 'joao@email.com')
  
  // Submeter formulário
  await page.click('button[type="submit"]')
  
  // Verificar se usuário foi criado
  await expect(page.locator('text=Usuário criado com sucesso')).toBeVisible()
  
  // Verificar se aparece na lista
  await expect(page.locator('text=João Silva')).toBeVisible()
})

🎯 Estratégias de teste

1. Test Pyramid

// Estrutura de testes
// 70% Unit Tests
// 20% Integration Tests  
// 10% E2E Tests
 
// Unit Test - Componente isolado
describe('Button', () => {
  it('deve renderizar corretamente', () => {
    // Testa apenas o componente
  })
})
 
// Integration Test - Múltiplos componentes
describe('UserList', () => {
  it('deve carregar e exibir usuários', () => {
    // Testa componente + API + estado
  })
})
 
// E2E Test - Fluxo completo
describe('User Management', () => {
  it('deve permitir criar, editar e deletar usuário', () => {
    // Testa fluxo completo da aplicação
  })
})

2. AAA Pattern

// Arrange, Act, Assert
describe('Calculator', () => {
  it('deve somar dois números', () => {
    // Arrange - Preparar dados
    const a = 2
    const b = 3
    const expected = 5
    
    // Act - Executar ação
    const result = add(a, b)
    
    // Assert - Verificar resultado
    expect(result).toBe(expected)
  })
})

3. Given-When-Then

describe('User Authentication', () => {
  it('deve permitir login com credenciais válidas', () => {
    // Given - Dado que tenho credenciais válidas
    const validCredentials = {
      email: 'user@example.com',
      password: 'password123'
    }
    
    // When - Quando tento fazer login
    const result = login(validCredentials)
    
    // Then - Então devo ser autenticado
    expect(result.success).toBe(true)
    expect(result.user).toBeDefined()
  })
})

🔧 Mocks e Stubs

1. Mocking de APIs

// Mock de fetch
global.fetch = vi.fn()
 
describe('UserService', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })
 
  it('deve buscar usuários da API', async () => {
    // Mock da resposta da API
    const mockUsers = [
      { id: '1', name: 'João', email: 'joao@email.com' }
    ]
    
    ;(fetch as any).mockResolvedValueOnce({
      ok: true,
      json: () => Promise.resolve(mockUsers)
    })
    
    const users = await fetchUsers()
    
    expect(fetch).toHaveBeenCalledWith('/api/users')
    expect(users).toEqual(mockUsers)
  })
})

2. Mocking de hooks

// Mock de hook customizado
vi.mock('@/hooks/useAuth', () => ({
  useAuth: () => ({
    user: { id: '1', name: 'João' },
    isAuthenticated: true,
    login: vi.fn(),
    logout: vi.fn()
  })
}))
 
describe('ProtectedRoute', () => {
  it('deve renderizar conteúdo para usuário autenticado', () => {
    render(
      <ProtectedRoute>
        <div>Conteúdo protegido</div>
      </ProtectedRoute>
    )
    
    expect(screen.getByText('Conteúdo protegido')).toBeInTheDocument()
  })
})

3. Mocking de módulos

// Mock de módulo externo
vi.mock('axios', () => ({
  default: {
    get: vi.fn(),
    post: vi.fn(),
    put: vi.fn(),
    delete: vi.fn()
  }
}))
 
// Mock de função específica
vi.mock('@/utils/formatDate', () => ({
  formatDate: vi.fn((date) => '2025-01-15')
}))

🎨 Testes de UI

1. Testes de acessibilidade

import { render, screen } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
 
expect.extend(toHaveNoViolations)
 
describe('Button Accessibility', () => {
  it('deve não ter violações de acessibilidade', async () => {
    const { container } = render(<Button>Clique aqui</Button>)
    const results = await axe(container)
    expect(results).toHaveNoViolations()
  })
 
  it('deve ter role button', () => {
    render(<Button>Clique aqui</Button>)
    expect(screen.getByRole('button')).toBeInTheDocument()
  })
 
  it('deve ter aria-label quando necessário', () => {
    render(<Button aria-label="Fechar modal">×</Button>)
    expect(screen.getByLabelText('Fechar modal')).toBeInTheDocument()
  })
})

2. Testes de responsividade

describe('ResponsiveLayout', () => {
  it('deve renderizar layout mobile', () => {
    // Mock de viewport mobile
    Object.defineProperty(window, 'innerWidth', {
      writable: true,
      configurable: true,
      value: 375
    })
    
    render(<ResponsiveLayout />)
    expect(screen.getByTestId('mobile-menu')).toBeInTheDocument()
  })
 
  it('deve renderizar layout desktop', () => {
    // Mock de viewport desktop
    Object.defineProperty(window, 'innerWidth', {
      writable: true,
      configurable: true,
      value: 1024
    })
    
    render(<ResponsiveLayout />)
    expect(screen.getByTestId('desktop-sidebar')).toBeInTheDocument()
  })
})

🚀 Performance Testing

1. Testes de performance

import { render } from '@testing-library/react'
import { performance } from 'perf_hooks'
 
describe('Performance', () => {
  it('deve renderizar lista grande rapidamente', () => {
    const start = performance.now()
    
    render(<LargeList items={Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }))} />)
    
    const end = performance.now()
    const renderTime = end - start
    
    expect(renderTime).toBeLessThan(100) // Menos de 100ms
  })
})

2. Testes de memory leaks

describe('Memory Leaks', () => {
  it('deve limpar event listeners', () => {
    const addEventListenerSpy = vi.spyOn(window, 'addEventListener')
    const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
    
    const { unmount } = render(<ComponentWithListeners />)
    unmount()
    
    expect(removeEventListenerSpy).toHaveBeenCalled()
  })
})

📊 Coverage e relatórios

1. Configuração de coverage

// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*'
      ],
      thresholds: {
        global: {
          branches: 80,
          functions: 80,
          lines: 80,
          statements: 80
        }
      }
    }
  }
})

2. Scripts de teste

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest --coverage",
    "test:watch": "vitest --watch",
    "test:ci": "vitest --run --coverage"
  }
}

🔄 CI/CD com testes

1. GitHub Actions

# .github/workflows/test.yml
name: Tests
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'pnpm'
          
      - name: Install dependencies
        run: pnpm install
        
      - name: Run tests
        run: pnpm test:ci
        
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage/lcov.info

2. Testes em paralelo

// vitest.config.ts
export default defineConfig({
  test: {
    pool: 'threads',
    poolOptions: {
      threads: {
        singleThread: false
      }
    }
  }
})

✅ Checklist de testes

Configuração:

Testes unitários:

Testes de integração:

Testes E2E:

CI/CD:


🎯 Conclusão

Testes automatizados não são apenas sobre encontrar bugs, são sobre criar confiança para evoluir o código. As estratégias mostradas aqui vão transformar sua abordagem ao desenvolvimento.

Principais benefícios:

Próximos passos:

  1. Configure testes no seu projeto
  2. Comece com testes unitários
  3. Adicione testes de integração
  4. Implemente E2E para fluxos críticos

Lembre-se: testes são um investimento, não um custo! 🚀

Allisson Lima
Desenvolvedor Frontend | Especialista em Qualidade e Testes