Testes - Frontend FinBoost+
Visão Geral
Estratégia robusta de testes automatizados usando Vitest + React Testing Library para garantir qualidade e confiabilidade do código frontend.
Stack de Testes
Ferramenta | Versão | Propósito |
---|---|---|
Vitest | ^1.0.0 |
Framework principal |
React Testing Library | ^14.0.0 |
Testes de componentes |
@testing-library/jest-dom | ^6.0.0 |
Matchers customizados |
@testing-library/user-event | ^14.0.0 |
Simulação de interações |
jsdom | ^23.0.0 |
Ambiente DOM virtual |
Estrutura do Projeto
__tests__/
├── components/ # Testes de componentes
│ ├── Header.test.jsx
│ ├── Button.test.jsx
│ └── Logo.test.jsx
├── integration/ # Testes de integração
│ └── LoginForm.test.jsx
├── setup.js # Configuração global
└── test-utils.js # Utilitários e helpers
Comandos
Padrões de Teste
1. Teste Básico
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import MeuComponente from '../../src/components/MeuComponente';
describe('MeuComponente', () => {
it('deve renderizar corretamente', () => {
render(<MeuComponente />);
expect(screen.getByText('Texto esperado')).toBeInTheDocument();
});
});
2. Interações do Usuário
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import Botao from '../../src/components/Botao';
describe('Botão com Click', () => {
it('deve chamar função ao clicar', async () => {
const user = userEvent.setup();
const mockClick = vi.fn();
render(<Botao onClick={mockClick}>Clique aqui</Botao>);
await user.click(screen.getByRole('button'));
expect(mockClick).toHaveBeenCalledTimes(1);
});
});
3. Context API
import { renderWithProviders } from '../test-utils';
describe('Componente com Contexto', () => {
it('deve usar dados do contexto', () => {
const mockUser = { name: 'João', id: 1 };
renderWithProviders(
<ComponenteComContexto />,
{ authContext: { user: mockUser } }
);
expect(screen.getByText('Olá, João')).toBeInTheDocument();
});
});
4. Hooks Customizados
import { renderHook, act } from '@testing-library/react';
import useContador from '../../src/hooks/useContador';
describe('useContador', () => {
it('deve incrementar contador', () => {
const { result } = renderHook(() => useContador(0));
act(() => { result.current.incrementar(); });
expect(result.current.contador).toBe(1);
});
});
Queries e Matchers
Estratégias de Busca (Por Prioridade)
Principais Matchers
Categoria | Matcher | Exemplo |
---|---|---|
Presença | toBeInTheDocument() |
expect(element).toBeInTheDocument() |
toBeVisible() |
expect(element).toBeVisible() |
|
Conteúdo | toHaveTextContent() |
expect(element).toHaveTextContent('Texto') |
toHaveValue() |
expect(input).toHaveValue('valor') |
|
Atributos | toHaveAttribute() |
expect(link).toHaveAttribute('href', '/home') |
toHaveClass() |
expect(button).toHaveClass('btn-primary') |
|
Estado | toBeDisabled() |
expect(input).toBeDisabled() |
toBeChecked() |
expect(checkbox).toBeChecked() |
|
Mocks | toHaveBeenCalled() |
expect(mockFn).toHaveBeenCalled() |
toHaveBeenCalledWith() |
expect(mockFn).toHaveBeenCalledWith(args) |
Mocks e Simulações
Mock de Funções
Mock de Módulos
// Mock completo
vi.mock('../../src/api', () => ({
default: {
fetchUser: vi.fn(),
createUser: vi.fn(),
}
}));
// Mock parcial
vi.mock('../../src/utils', async () => {
const actual = await vi.importActual('../../src/utils');
return {
...actual,
formatDate: vi.fn().mockReturnValue('01/01/2024'),
};
});
Testes de Integração
Formulários Completos
Exemplo: Login Form
describe('LoginForm - Integração', () => {
it('deve submeter formulário com dados válidos', async () => {
const user = userEvent.setup();
const mockSubmit = vi.fn().mockResolvedValue({
success: true, token: 'mock-token'
});
render(<LoginForm onSubmit={mockSubmit} />);
// Preencher
await user.type(screen.getByLabelText(/email/i), 'user@test.com');
await user.type(screen.getByLabelText(/senha/i), 'senha123');
// Submeter
await user.click(screen.getByRole('button', { name: /entrar/i }));
// Verificar
expect(mockSubmit).toHaveBeenCalledWith({
email: 'user@test.com',
password: 'senha123'
});
});
});
Estados Assíncronos
describe('ComponenteAssincrono', () => {
it('deve mostrar loading e depois conteúdo', async () => {
const mockFetch = vi.fn().mockResolvedValue({
data: { message: 'Dados carregados!' }
});
render(<ComponenteAssincrono fetchData={mockFetch} />);
// Loading
expect(screen.getByText(/carregando/i)).toBeInTheDocument();
// Aguardar
await waitFor(() => {
expect(screen.queryByText(/carregando/i)).not.toBeInTheDocument();
});
// Conteúdo
expect(screen.getByText('Dados carregados!')).toBeInTheDocument();
});
});
Boas Práticas
Padrão AAA
Arrange, Act, Assert
it('deve calcular total corretamente', async () => {
// ✅ Arrange - Preparar cenário
const user = userEvent.setup();
const produtos = [
{ id: 1, nome: 'Produto A', preco: 10.00 },
{ id: 2, nome: 'Produto B', preco: 15.50 }
];
// ✅ Act - Executar ação
render(<CarrinhoCompras produtos={produtos} />);
await user.click(screen.getByText('Calcular Total'));
// ✅ Assert - Verificar resultado
expect(screen.getByText('Total: R$ 25,50')).toBeInTheDocument();
});
Nomes Descritivos
describe('ButtonUI', () => {
// ❌ Ruim
it('testa botão', () => {});
// ❌ Testa implementação
it('deve ter className btn-primary', () => {});
// ✅ Bom - descreve comportamento
it('deve chamar onSubmit quando formulário é submetido', () => {});
// ✅ Específico e claro
it('deve mostrar spinner quando loading é true', () => {});
});
Comportamento vs Implementação
// ❌ Evitar - testa implementação
expect(component.state.isLoading).toBe(true);
// ✅ Preferir - testa comportamento
expect(screen.getByRole('status', { name: /carregando/i }))
.toBeInTheDocument();
Solução de Problemas
Problemas Comuns
ResizeObserver Error
// Já configurado no setup.js
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
Elemento não encontrado
Cobertura de Código
Configuração
// vite.config.js
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json'],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
}
})
CI/CD
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage
Recursos
Links Úteis
VS Code Extensions
- Vitest Runner
- Testing Library Snippets
- Jest Snippets
Lembre-se
Teste comportamentos, não implementação. Seus testes devem ser resilientes a refatorações e mudanças internas, focando na experiência do usuário final.