Voltar para o Blog

Testes Automatizados no Front-end: Guia Completo com Vitest e Playwright

Olá HaWkers, testes automatizados são uma das habilidades mais valorizadas para desenvolvedores front-end em 2025. Com aplicações cada vez mais complexas, garantir qualidade sem testes manuais exaustivos se tornou impossível.

Você já lançou uma feature achando que estava tudo certo, só para descobrir que quebrou algo em outra parte da aplicação? Vamos resolver isso de vez.

Por Que Testar o Front-end em 2025

O ecossistema de testes JavaScript amadureceu significativamente. Ferramentas modernas como Vitest e Playwright tornaram os testes mais rápidos e agradáveis de escrever.

O Custo de Não Testar

Estatísticas de mercado:

  • Bugs em produção custam 10x mais para corrigir que em desenvolvimento
  • 40% do tempo de desenvolvimento é gasto corrigindo bugs
  • Empresas com boa cobertura de testes lançam features 2x mais rápido
  • Code reviews são 50% mais rápidas quando há testes

A pirâmide de testes:

Tipo Quantidade Velocidade Custo
Unitários Muitos Muito rápido Baixo
Integração Moderado Rápido Médio
E2E Poucos Lento Alto

Configurando Vitest

Vitest se tornou o padrão para testes unitários e de integração em projetos JavaScript modernos. Ele é compatível com a API do Jest mas significativamente mais rápido.

Setup Inicial

# Instalar Vitest e dependências
npm install -D vitest @vitest/coverage-v8

# Para React
npm install -D @testing-library/react @testing-library/jest-dom jsdom

# Para Vue
npm install -D @vue/test-utils @testing-library/vue jsdom

Configuração

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

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'src/test/',
        '**/*.d.ts',
        '**/*.config.*',
      ],
    },
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

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

Testes Unitários com Vitest

Testando Funções Utilitárias

// src/utils/formatters.ts
export function formatCurrency(value: number, locale = 'pt-BR'): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: locale === 'pt-BR' ? 'BRL' : 'USD',
  }).format(value);
}

export function formatDate(date: Date | string, locale = 'pt-BR'): string {
  const d = typeof date === 'string' ? new Date(date) : date;
  return new Intl.DateTimeFormat(locale, {
    day: '2-digit',
    month: '2-digit',
    year: 'numeric',
  }).format(d);
}

export function slugify(text: string): string {
  return text
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^\w\s-]/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-')
    .trim();
}
// src/utils/formatters.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency, formatDate, slugify } from './formatters';

describe('formatCurrency', () => {
  it('formata valores em Real brasileiro por padrão', () => {
    expect(formatCurrency(1234.56)).toBe('R$ 1.234,56');
  });

  it('formata valores em Dólar americano', () => {
    expect(formatCurrency(1234.56, 'en-US')).toBe('$1,234.56');
  });

  it('formata valores negativos corretamente', () => {
    expect(formatCurrency(-500)).toBe('-R$ 500,00');
  });

  it('formata zero corretamente', () => {
    expect(formatCurrency(0)).toBe('R$ 0,00');
  });
});

describe('formatDate', () => {
  it('formata Date object em pt-BR', () => {
    const date = new Date('2025-11-22');
    expect(formatDate(date)).toBe('22/11/2025');
  });

  it('formata string ISO em pt-BR', () => {
    expect(formatDate('2025-11-22')).toBe('22/11/2025');
  });

  it('formata em en-US quando especificado', () => {
    expect(formatDate('2025-11-22', 'en-US')).toBe('11/22/2025');
  });
});

describe('slugify', () => {
  it('converte texto simples para slug', () => {
    expect(slugify('Hello World')).toBe('hello-world');
  });

  it('remove acentos', () => {
    expect(slugify('Programação em Português')).toBe('programacao-em-portugues');
  });

  it('remove caracteres especiais', () => {
    expect(slugify('Test@#$%String!')).toBe('teststring');
  });

  it('normaliza múltiplos espaços e hífens', () => {
    expect(slugify('Multiple   Spaces---Here')).toBe('multiple-spaces-here');
  });
});

Testando Componentes React

// src/components/Button.tsx
import { ButtonHTMLAttributes, ReactNode } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  isLoading?: boolean;
  children: ReactNode;
}

export function Button({
  variant = 'primary',
  isLoading = false,
  children,
  disabled,
  ...props
}: ButtonProps) {
  const variants = {
    primary: 'bg-blue-500 hover:bg-blue-600 text-white',
    secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
    danger: 'bg-red-500 hover:bg-red-600 text-white',
  };

  return (
    <button
      className={`px-4 py-2 rounded font-medium transition-colors ${variants[variant]} ${
        (disabled || isLoading) && 'opacity-50 cursor-not-allowed'
      }`}
      disabled={disabled || isLoading}
      {...props}
    >
      {isLoading ? (
        <span className="flex items-center gap-2">
          <span className="animate-spin">⏳</span>
          Carregando...
        </span>
      ) : (
        children
      )}
    </button>
  );
}
// src/components/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renderiza o texto do children', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('chama onClick quando clicado', () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    fireEvent.click(screen.getByText('Click me'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('não chama onClick quando disabled', () => {
    const handleClick = vi.fn();
    render(
      <Button onClick={handleClick} disabled>
        Click me
      </Button>
    );

    fireEvent.click(screen.getByText('Click me'));

    expect(handleClick).not.toHaveBeenCalled();
  });

  it('mostra estado de loading', () => {
    render(<Button isLoading>Submit</Button>);

    expect(screen.getByText('Carregando...')).toBeInTheDocument();
    expect(screen.queryByText('Submit')).not.toBeInTheDocument();
  });

  it('desabilita o botão quando isLoading', () => {
    render(<Button isLoading>Submit</Button>);

    expect(screen.getByRole('button')).toBeDisabled();
  });

  it('aplica classes de variante corretamente', () => {
    const { rerender } = render(<Button variant="primary">Test</Button>);
    expect(screen.getByRole('button')).toHaveClass('bg-blue-500');

    rerender(<Button variant="danger">Test</Button>);
    expect(screen.getByRole('button')).toHaveClass('bg-red-500');
  });
});

Testando Hooks Customizados

// src/hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore =
        value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue] as const;
}
// src/hooks/useLocalStorage.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useLocalStorage } from './useLocalStorage';

describe('useLocalStorage', () => {
  beforeEach(() => {
    localStorage.clear();
    vi.clearAllMocks();
  });

  it('retorna valor inicial quando localStorage está vazio', () => {
    const { result } = renderHook(() => useLocalStorage('test-key', 'default'));

    expect(result.current[0]).toBe('default');
  });

  it('retorna valor do localStorage quando existe', () => {
    localStorage.setItem('test-key', JSON.stringify('stored-value'));

    const { result } = renderHook(() => useLocalStorage('test-key', 'default'));

    expect(result.current[0]).toBe('stored-value');
  });

  it('atualiza valor no state e localStorage', () => {
    const { result } = renderHook(() => useLocalStorage('test-key', 'initial'));

    act(() => {
      result.current[1]('updated');
    });

    expect(result.current[0]).toBe('updated');
    expect(JSON.parse(localStorage.getItem('test-key')!)).toBe('updated');
  });

  it('suporta função de atualização', () => {
    const { result } = renderHook(() => useLocalStorage('counter', 0));

    act(() => {
      result.current[1]((prev) => prev + 1);
    });

    expect(result.current[0]).toBe(1);
  });

  it('funciona com objetos complexos', () => {
    const initialUser = { name: 'João', age: 25 };
    const { result } = renderHook(() => useLocalStorage('user', initialUser));

    act(() => {
      result.current[1]({ ...result.current[0], age: 26 });
    });

    expect(result.current[0]).toEqual({ name: 'João', age: 26 });
  });
});

Testes E2E com Playwright

Playwright é a ferramenta mais completa para testes end-to-end em 2025. Suporta múltiplos browsers e é significativamente mais rápido que o Cypress.

Configuração

npm init playwright@latest
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Testes E2E Práticos

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

test.describe('Autenticação', () => {
  test('usuário pode fazer login com credenciais válidas', async ({ page }) => {
    await page.goto('/login');

    // Preenche formulário
    await page.getByLabel('Email').fill('usuario@exemplo.com');
    await page.getByLabel('Senha').fill('senha123');

    // Submete
    await page.getByRole('button', { name: 'Entrar' }).click();

    // Verifica redirecionamento
    await expect(page).toHaveURL('/dashboard');

    // Verifica elemento de usuário logado
    await expect(page.getByText('Bem-vindo, Usuário')).toBeVisible();
  });

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

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

    await expect(page.getByText('Email ou senha inválidos')).toBeVisible();
    await expect(page).toHaveURL('/login');
  });

  test('redireciona usuário não autenticado para login', async ({ page }) => {
    await page.goto('/dashboard');

    await expect(page).toHaveURL('/login?redirect=/dashboard');
  });
});
// e2e/shopping-cart.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Carrinho de Compras', () => {
  test.beforeEach(async ({ page }) => {
    // Login antes de cada teste
    await page.goto('/login');
    await page.getByLabel('Email').fill('usuario@exemplo.com');
    await page.getByLabel('Senha').fill('senha123');
    await page.getByRole('button', { name: 'Entrar' }).click();
    await expect(page).toHaveURL('/dashboard');
  });

  test('adiciona produto ao carrinho', async ({ page }) => {
    await page.goto('/produtos');

    // Clica no primeiro produto
    await page.getByTestId('produto-card').first().click();

    // Adiciona ao carrinho
    await page.getByRole('button', { name: 'Adicionar ao Carrinho' }).click();

    // Verifica contador do carrinho
    await expect(page.getByTestId('cart-count')).toHaveText('1');
  });

  test('atualiza quantidade no carrinho', async ({ page }) => {
    // Adiciona produto primeiro
    await page.goto('/produtos/1');
    await page.getByRole('button', { name: 'Adicionar ao Carrinho' }).click();

    // Vai para o carrinho
    await page.goto('/carrinho');

    // Aumenta quantidade
    await page.getByRole('button', { name: 'Aumentar quantidade' }).click();

    // Verifica quantidade atualizada
    await expect(page.getByTestId('item-quantity')).toHaveValue('2');
  });

  test('completa checkout com sucesso', async ({ page }) => {
    // Setup: adiciona produto
    await page.goto('/produtos/1');
    await page.getByRole('button', { name: 'Adicionar ao Carrinho' }).click();
    await page.goto('/carrinho');

    // Inicia checkout
    await page.getByRole('button', { name: 'Finalizar Compra' }).click();

    // Preenche dados de entrega
    await page.getByLabel('CEP').fill('01310-100');
    await page.getByRole('button', { name: 'Buscar CEP' }).click();
    await expect(page.getByLabel('Rua')).toHaveValue(/Paulista/);

    await page.getByLabel('Número').fill('1000');

    // Seleciona forma de pagamento
    await page.getByLabel('Cartão de Crédito').check();
    await page.getByLabel('Número do Cartão').fill('4111111111111111');
    await page.getByLabel('Validade').fill('12/28');
    await page.getByLabel('CVV').fill('123');

    // Confirma
    await page.getByRole('button', { name: 'Confirmar Pedido' }).click();

    // Verifica sucesso
    await expect(page).toHaveURL(/\/pedido\/\d+/);
    await expect(page.getByText('Pedido realizado com sucesso')).toBeVisible();
  });
});

Padrões e Boas Práticas

Page Object Model

// e2e/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Senha');
    this.submitButton = page.getByRole('button', { name: 'Entrar' });
    this.errorMessage = page.getByRole('alert');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
}

// Uso no teste
test('login com page object', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('usuario@exemplo.com', 'senha123');

  await expect(page).toHaveURL('/dashboard');
});

Fixtures Customizados

// e2e/fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

type Fixtures = {
  loginPage: LoginPage;
  authenticatedPage: void;
};

export const test = base.extend<Fixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  authenticatedPage: async ({ page }, use) => {
    // Setup: faz login
    await page.goto('/login');
    await page.getByLabel('Email').fill('usuario@exemplo.com');
    await page.getByLabel('Senha').fill('senha123');
    await page.getByRole('button', { name: 'Entrar' }).click();
    await page.waitForURL('/dashboard');

    // Executa o teste
    await use();

    // Cleanup: pode fazer logout ou limpar dados
  },
});

export { expect } from '@playwright/test';

Conclusão

Testes automatizados no front-end deixaram de ser um luxo para se tornarem necessidade. Com Vitest e Playwright, você tem ferramentas modernas, rápidas e agradáveis de usar.

Comece com:

  1. Testes unitários para funções utilitárias
  2. Testes de componentes para UI crítica
  3. Alguns testes E2E para fluxos principais

Não tente ter 100% de cobertura imediatamente. Foque em testar o que mais importa e vá expandindo gradualmente.

Se você quer aprofundar seus conhecimentos em JavaScript e suas melhores práticas, recomendo que dê uma olhada em outro artigo: ECMAScript 2025: Os Novos Recursos do JavaScript onde você vai descobrir as novidades da linguagem.

Bora pra cima! 🦅

📚 Quer Aprofundar Seus Conhecimentos em JavaScript?

Este artigo cobriu testes, mas há muito mais para explorar no mundo do desenvolvimento moderno.

Desenvolvedores que investem em conhecimento sólido e estruturado tendem a ter mais oportunidades no mercado.

Material de Estudo Completo

Se você quer dominar JavaScript do básico ao avançado, preparei um guia completo:

Opções de investimento:

  • 1x de R$9,90 no cartão
  • ou R$9,90 à vista

👉 Conhecer o Guia JavaScript

💡 Material atualizado com as melhores práticas do mercado

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário