Dogwalk amadurece — testes Playwright como backbone de qualidade
🐶 Dogwalk·

Dogwalk amadurece — testes Playwright como backbone de qualidade

📖 9 min de leitura← Voltar para timeline

Contexto

O Dogwalk nasceu como um MVP — mínimo, viável, e sem testes. Faz parte. Quando você tá validando ideia, teste é luxo. Mas quando o marketplace começa a movimentar dinheiro de verdade (via Stripe Connect), quando tutores confiam dados dos pets, quando walkers dependem da plataforma pra agenda — aí teste vira necessidade, não luxo.

Esse post conta a jornada de 0 → 853 testes, o que aprendi no caminho, e como o Playwright virou o backbone de qualidade do Dogwalk.

Fase 1: O caos sem testes

No começo era só React + Vite, prototipação rápida. Zero testes. Zero tipos. Zero garantias.

O fluxo era: escrever feature, abrir localhost:5173, clicar manualmente, ver se não quebrou. Funcionava enquanto o app tinha 3 páginas. Quando passou de 10 páginas + autenticação + Stripe + agendamento, já não dava mais.

// Exemplo real: primeiro teste que escrevi — validar que o app renderiza sem crash
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import App from './App';

describe('App', () => {
  it('renders without crashing', () => {
    render(<App />);
    expect(screen.getByText(/dogwalk/i)).toBeDefined();
  });
});

Ingênuo? Sim. Mas foi o primeiro passo.

Fase 2: Vitest nos componentes críticos

Comecei pelo core: componentes de autenticação, formulários de agendamento, cálculo de preços (comissão Stripe + taxa walker).

// Teste de cálculo de comissão — dinheiro não pode errar
describe('CommissionCalculator', () => {
  it('calculates 15% platform fee correctly', () => {
    const result = calculateCommission(100.00);
    expect(result.platformFee).toBe(15.00);
    expect(result.walkerEarns).toBe(85.00);
    expect(result.total).toBe(100.00);
  });

  it('handles Stripe Connect fee + platform fee stacking', () => {
    const result = calculateWithStripe(85.00, 'connect');
    // Stripe Connect: 2.9% + R$0.49
    expect(result.stripeFee).toBeCloseTo(2.96, 1);
    expect(result.walkerNet).toBeCloseTo(82.04, 1);
  });

  it('throws on negative values', () => {
    expect(() => calculateCommission(-50)).toThrow();
  });
});

Essa fase foi produtiva — 342 testes Vitest cobrindo:

  • Componentes de UI (renderização, interação, estados loading/empty/error)
  • Hooks customizados (useAuth, useStripe, useSchedule)
  • Utilitários (formatação, cálculo, validação)
  • Stores de estado (Zustand)

Tempo de execução: 12s. Rápido o suficiente pra rodar antes de cada commit.

Fase 3: Playwright E2E — o game changer

Teste unitário garante que o componente funciona. Mas não garante que o fluxo inteiro roda. Um erro de autenticação que só aparece quando o usuário faz login, agenda um passeio, e tenta pagar — nenhum teste unitário pega isso.

Foi aí que entrei no Playwright.

// Teste E2E: fluxo completo de agendamento
import { test, expect } from '@playwright/test';

test('tutor agenda passeio completo: login → search → schedule → pay', async ({ page }) => {
  // Login
  await page.goto('/login');
  await page.fill('[data-testid="email"]', '[email protected]');
  await page.fill('[data-testid="password"]', 'senha_teste');
  await page.click('[data-testid="login-btn"]');
  await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();

  // Busca walker
  await page.goto('/search');
  await page.fill('[data-testid="search-input"]', 'Pinheiros');
  await page.click('[data-testid="search-btn"]');
  await page.locator('[data-testid="walker-card"]').first().click();

  // Agenda
  await page.fill('[data-testid="date-input"]', '2026-03-15');
  await page.fill('[data-testid="time-input"]', '14:00');
  await page.click('[data-testid="schedule-btn"]');
  await expect(page.locator('[data-testid="booking-confirmed"]')).toBeVisible();

  // Pagamento
  await page.click('[data-testid="pay-btn"]');
  await page.fill('[data-testid="card-number"]', '4242424242424242');
  await page.fill('[data-testid="card-expiry"]', '12/28');
  await page.fill('[data-testid="card-cvc"]', '123');
  await page.click('[data-testid="confirm-payment"]');
  await expect(page.locator('[data-testid="payment-success"]')).toBeVisible();
});

Esse único teste já substituiu 10 minutos de teste manual.

O que os E2E cobrem hoje (511 testes):

Grupo Testes O que testa
Auth 42 Login, registro, OAuth, refresh, logout, sessão expirada
Dashboard 38 Cards tutor/walker, métricas, health checks
Search 55 Busca, filtros, geolocalização, empty states
Scheduling 67 Agenda, conflito, cancelamento, re-agendamento
Payment 73 Stripe Connect, cartão, PIX, split payments, refund
Profile 41 Edição tutor, edição walker, foto, endereço
Admin 29 Painel admin, usuários, transações, reports
Notifications 23 Push, email, in-app, preferências
Mobile 143 Responsivo 360px, touch targets, bottom nav, swipes

Métricas reais

Métrica Sem testes Com Vitest Com Playwright
Tempo de execução N/A 12s 4min 23s
Cobertura estimada 0% ~45% ~85% fluxos
Bugs em produção ~8/mês ~3/mês 0-1/mês
Regressão por release 100% ~40% ~5%
Confiança pra deploy Baixa Média Alta
CI duration 30s 45s 6min 12s

O custo em tempo de CI (6 minutos vs 30 segundos) é real. Mas o ganho em confiança compensa — especialmente em fluxos que envolvem dinheiro.

Aprendizados da trincheira

1. Data-testid salva sua vida

No começo tentava selecionar por texto, classe CSS, placeholder. Tudo quebrava no mínimo refactor. Depois que padronizei data-testid em todo elemento interativo, os testes ficaram resilientes.

// Antes: quebrava se mudasse o texto
<button className="btn-primary">Agendar passeio</button>

// Depois: não quebra nunca
<button data-testid="schedule-btn">Agendar passeio</button>

2. Teste E2E lento não é executado

Se o teste leva 6 minutos, o desenvolvedor não roda localmente. Solução: separar em smoke tests (1 minuto, rodam sempre) e full suite (6 minutos, só no CI e antes de release).

# Smoke — rápido, roda local
npx playwright test --grep @smoke

# Full — completo, só CI
npx playwright test

3. Stripe nos testes é um problema à parte

Stripe Connect exige cartão real pra testar split payments. Solução: usar stripe-cli com forwarding de webhooks + 4242 card no modo test. O truque é nunca testar em produção.

# Forward webhooks do Stripe test pra máquina local
stripe listen --forward-to localhost:5173/api/stripe/webhook

4. Teste mobile-first

O Dogwalk tem mais usuários mobile que desktop. 143 testes mobile vs 368 desktop. Usei test.use({ viewport: { width: 375, height: 812 } }) como padrão e só sobrescrevo quando testar desktop.

5. Screenshots em toda falha

Playwright tira screenshot automático em falha. Mas adicionei também um trace: 'on-first-retry' pro caso de teste flaky — o trace mostra cada ação, network request, e console error.

// playwright.config.ts
export default defineConfig({
  use: {
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
    video: 'retain-on-failure',
  },
});

Os números frios

Métrica Antes Depois
Testes totais 0 853
Testes Vitest 0 342
Testes Playwright 0 511
Bugs em produção ~8/mês 0-1/mês
CI duration 30s 6min
Cobertura fluxos críticos 0% 100%
Confiança de deploy “reza” “só vai”

O que vem a seguir

Os 853 testes são um bom número, mas ainda tem lacunas:

  • Testes de performance — Lighthouse CI pra medir impacto de bundle
  • Testes de Stripe Connect — cenários de chargeback e dispute
  • Testes de acessibilidade — axe-core integrado no Playwright
  • Testes visuais — snapshot comparativo pra detectar regressão de layout

Mas por enquanto, 853 testes, 0 bugs críticos nos últimos 30 dias. Tô satisfeito.


Comandos úteis

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$