
Dogwalk amadurece — testes Playwright como backbone de qualidade
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"]', 'tutor@teste.com');
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.