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:
- 🐛 90% menos bugs em produção
- 🔄 Refatoração segura e confiável
- 📚 Documentação viva do código
- 🚀 Deploy com confiança
- 👥 Colaboração mais eficiente
- ⚡ Desenvolvimento mais rápido
Estatísticas impressionantes:
- 78% dos projetos com testes têm menos bugs
- 65% redução no tempo de debugging
- 40% melhoria na velocidade de desenvolvimento
🛠️ 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.info2. Testes em paralelo
// vitest.config.ts
export default defineConfig({
test: {
pool: 'threads',
poolOptions: {
threads: {
singleThread: false
}
}
}
})✅ Checklist de testes
Configuração:
- Vitest ou Jest configurado
- Testing Library instalado
- Setup de testes criado
- Scripts de teste configurados
- Coverage configurado
Testes unitários:
- Componentes testados
- Hooks testados
- Utilitários testados
- Mocks configurados
- Edge cases cobertos
Testes de integração:
- Fluxos de usuário testados
- APIs mockadas
- Estado gerenciado
- Navegação testada
- Formulários testados
Testes E2E:
- Fluxos críticos testados
- Cross-browser testing
- Mobile testing
- Performance testing
- Acessibilidade testada
CI/CD:
- Testes rodam no CI
- Coverage reportado
- Testes em paralelo
- Falhas bloqueiam merge
- Relatórios enviados
🎯 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:
- 🐛 Menos bugs em produção
- 🔄 Refatoração segura
- 📚 Documentação viva
- 🚀 Deploy confiável
- 👥 Colaboração eficiente
Próximos passos:
- Configure testes no seu projeto
- Comece com testes unitários
- Adicione testes de integração
- 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