Testes no Frontend

Testes no Frontend

Testes no frontend evoluíram drasticamente. A era de testes frágeis baseados em seletores CSS e snapshot testing excessivo deu lugar a uma abordagem centrada no usuário: testar o que o usuário vê e faz, não os detalhes de implementação. Testing Library mudou a filosofia e Vitest trouxe velocidade. Esta lição cobre a stack moderna de testes para aplicações React.


1. Filosofia: Teste Comportamento, Não Implementação

// ❌ Teste de implementação — frágil
test('incrementa o state counter', () => {
  const { result } = renderHook(() => useState(0));
  act(() => result.current[1](1));
  expect(result.current[0]).toBe(1);
  // Quebra se você mudar de useState para useReducer
});

// ✅ Teste de comportamento — resiliente
test('incrementa o contador quando o botão é clicado', async () => {
  render(<Counter />);
  expect(screen.getByText('Contagem: 0')).toBeInTheDocument();

  await userEvent.click(screen.getByRole('button', { name: /incrementar/i }));
  expect(screen.getByText('Contagem: 1')).toBeInTheDocument();
  // Funciona independente da implementação interna
});

Princípio do Testing Library: “The more your tests resemble the way your software is used, the more confidence they can give you.” — Kent C. Dodds


2. Setup: Vitest + Testing Library

2.1 Configuração

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',          // Simula browser
    globals: true,                  // describe, test, expect globais
    setupFiles: './src/test/setup.ts',
    css: true,                      // Processa CSS modules
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      exclude: ['**/*.test.*', '**/test/**'],
    },
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom/vitest'; // Matchers como toBeInTheDocument()
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

afterEach(() => {
  cleanup(); // Limpa o DOM após cada teste
});

3. Queries do Testing Library

3.1 Prioridade de Queries

Testing Library define uma hierarquia de queries — use a mais acessível possível:

PrioridadeQueryQuando usar
1getByRoleSempre que possível (acessível)
2getByLabelTextInputs de formulário
3getByPlaceholderTextQuando não há label
4getByTextConteúdo textual
5getByDisplayValueValor atual de input
6getByAltTextImagens
7getByTestIdÚltimo recurso
// ✅ Acessível — funciona com screen readers
screen.getByRole('button', { name: /salvar/i });
screen.getByRole('heading', { level: 2 });
screen.getByLabelText(/email/i);

// ⚠️ Aceitável quando role não é suficiente
screen.getByText(/bem-vindo/i);

// ❌ Último recurso — não tem significado semântico
screen.getByTestId('submit-button');

3.2 Variantes: get, query, find

// getBy — lança erro se não encontrar (default para a maioria dos testes)
screen.getByRole('button'); // Throws se não existir

// queryBy — retorna null se não encontrar (útil para testar ausência)
expect(screen.queryByText('Erro')).not.toBeInTheDocument();

// findBy — retorna Promise, espera o elemento aparecer (async)
const message = await screen.findByText('Dados carregados');
// Útil para dados que chegam de API

4. Interações com userEvent

import userEvent from '@testing-library/user-event';

test('formulário de login', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();

  render(<LoginForm onSubmit={onSubmit} />);

  // Digitar nos campos
  await user.type(screen.getByLabelText(/email/i), 'maria@test.com');
  await user.type(screen.getByLabelText(/senha/i), 'senha123');

  // Clicar no botão
  await user.click(screen.getByRole('button', { name: /entrar/i }));

  // Verificar chamada
  expect(onSubmit).toHaveBeenCalledWith({
    email: 'maria@test.com',
    password: 'senha123',
  });
});

test('seleção e keyboard', async () => {
  const user = userEvent.setup();
  render(<Autocomplete options={['React', 'Vue', 'Angular']} />);

  await user.type(screen.getByRole('combobox'), 'Re');
  await user.keyboard('{ArrowDown}{Enter}');

  expect(screen.getByRole('combobox')).toHaveValue('React');
});

5. Mocking de APIs com MSW

MSW (Mock Service Worker) intercepta requests na camada de rede — seus componentes fazem fetch normalmente sem saber que estão sendo mockados:

// src/test/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Maria',
      email: 'maria@test.com',
    });
  }),

  http.post('/api/login', async ({ request }) => {
    const body = await request.json();

    if (body.email === 'maria@test.com') {
      return HttpResponse.json({ token: 'fake-token' });
    }

    return HttpResponse.json(
      { error: 'Credenciais inválidas' },
      { status: 401 }
    );
  }),
];
// src/test/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

// src/test/setup.ts
import { server } from './server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Teste do componente — faz fetch real, MSW intercepta
test('exibe dados do usuário', async () => {
  render(<UserProfile userId="123" />);

  // findBy espera o elemento aparecer (após fetch)
  expect(await screen.findByText('Maria')).toBeInTheDocument();
  expect(screen.getByText('maria@test.com')).toBeInTheDocument();
});

// Override de handler para cenário de erro
test('exibe erro quando API falha', async () => {
  server.use(
    http.get('/api/users/:id', () => {
      return HttpResponse.json(null, { status: 500 });
    })
  );

  render(<UserProfile userId="123" />);
  expect(await screen.findByText(/erro ao carregar/i)).toBeInTheDocument();
});

6. Testes E2E com Playwright

Playwright testa em browsers reais com auto-waiting, screenshots e trace viewer:

// e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Fluxo de Login', () => {
  test('login com credenciais válidas', async ({ page }) => {
    await page.goto('/login');

    await page.getByLabel('Email').fill('maria@test.com');
    await page.getByLabel('Senha').fill('senha123');
    await page.getByRole('button', { name: 'Entrar' }).click();

    // Espera navegação automática
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByRole('heading', { name: 'Bem-vinda, Maria' })).toBeVisible();
  });

  test('exibe erro com credenciais inválidas', async ({ page }) => {
    await page.goto('/login');

    await page.getByLabel('Email').fill('wrong@test.com');
    await page.getByLabel('Senha').fill('wrong');
    await page.getByRole('button', { name: 'Entrar' }).click();

    await expect(page.getByText('Credenciais inválidas')).toBeVisible();
    await expect(page).toHaveURL('/login'); // Não navegou
  });
});

6.1 Playwright Config

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  timeout: 30000,
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',     // Trace para debugging
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

7. Patterns e Anti-Patterns

O que testar

  • Comportamento visível para o usuário (texto, interações, navegação)
  • Fluxos críticos de negócio (checkout, autenticação, formulários)
  • Edge cases (loading, erro, lista vazia, permissões)
  • Acessibilidade (roles corretos, labels, keyboard navigation)

O que NÃO testar

  • State interno de componentes (useState, useReducer)
  • Detalhes de implementação (nomes de funções internas, lifecycle)
  • Snapshot tests excessivos (frágeis, pouca confiança)
  • Estilos CSS específicos (use visual regression tools se necessário)

8. Referências e Aprofundamento

  • Testing Library Docs — guia oficial e queries
  • Vitest Documentation — configuração, API e integração com Vite
  • Playwright Documentation — E2E testing com auto-waiting e trace viewer
  • MSW (Mock Service Worker) — mocking de API na camada de rede
  • “Testing JavaScript” (Kent C. Dodds) — curso completo sobre testing no ecossistema JS
  • CS50 Web (Harvard) — seção sobre testing e CI

Referencias e Fontes

  • Testing Library Documentationhttps://testing-library.com — Documentacao oficial da Testing Library com guias sobre queries, eventos, boas praticas e integracao com frameworks
  • Jest Documentationhttps://jestjs.io — Documentacao oficial do Jest cobrindo configuracao, matchers, mocks, spies e integracao com projetos JavaScript e TypeScript
  • Playwright Documentationhttps://playwright.dev — Documentacao oficial do Playwright para testes end-to-end com auto-waiting, trace viewer e suporte multi-browser
  • MSW Documentationhttps://mswjs.io — Documentacao do Mock Service Worker para interceptacao de requisicoes na camada de rede, util para testes de integracao e desenvolvimento
  • “Testing JavaScript” — Kent C. Dodds — Curso completo e aprofundado sobre estrategias de teste no ecossistema JavaScript, desde testes unitarios ate E2E