Automação

Visual regression testing: quando o teste passa mas a tela está quebrada

O teste E2E clica no botão, confere que o texto certo apareceu e fica verde. Enquanto isso, em produção, um CSS quebrado empurrou esse botão para fora da tela, deixou o texto branco no fundo branco ou colapsou o menu inteiro. O teste passou porque ele verifica o DOM, não a aparência. Essa lacuna — funcional ✓, visual ✗ — é exatamente o que o teste de regressão visual existe para fechar.

O ponto cego dos testes funcionais

Um teste tradicional pergunta "o elemento existe e tem o conteúdo certo?". Ele consegue afirmar que <button>Comprar</button> está no DOM e é clicável. O que ele não vê: que o botão está com color igual ao background, que um z-index o escondeu atrás de outro elemento, que a fonte não carregou, ou que em 1280px de largura o card vazou para fora do container. Para o usuário, isso é o produto quebrado. Para a suíte funcional, é tudo verde.

Como funciona: baseline, comparação e diff

A ideia é direta: tire uma foto da tela e compare com uma foto aprovada anteriormente. O fluxo tem três passos:

  1. BASELINE      captura a tela "correta" e a guarda como referência
        │
  2. COMPARAÇÃO    a cada execução, tira novo screenshot e compara
        │          pixel a pixel com a baseline
        ▼
  3. DIFF          ┌─ idênticos  → passa
                   └─ diferentes → falha + imagem destacando o que mudou

Quando há diferença, a ferramenta gera uma imagem de diff realçando as regiões alteradas. Aí entra a decisão humana: se a mudança é um bug, você conserta; se é uma mudança intencional (você de fato redesenhou o botão), você aprova a nova baseline e ela vira a referência dali pra frente.

O grande inimigo: o falso positivo

Comparar pixel a pixel é poderoso e ingênuo ao mesmo tempo. Um monte de coisa muda a imagem sem que nada esteja quebrado — e cada uma vira um teste vermelho que não é bug:

Fonte de ruídoComo domar
Anti-aliasing de fontes entre SOsGerar baseline no mesmo ambiente (container/CI), não no laptop
Conteúdo dinâmico (datas, nomes, anúncios)Mascarar/ignorar a região no screenshot
Animações e carrosséisDesabilitar animações e esperar a tela estabilizar
Pequenas diferenças de renderizaçãoDefinir um threshold de tolerância (ex.: 0,1%)
Teste visual sem controle de falso positivo morre em duas semanas: o time se cansa de aprovar diffs que não são bugs e passa a clicar em "aprovar tudo" — momento em que o teste deixa de testar. Estabilidade da baseline não é detalhe, é o que mantém o teste vivo.

Pixel a pixel vs. comparação perceptual

As ferramentas mais novas não comparam pixels crus: usam diff perceptual, que ignora variações imperceptíveis ao olho humano (como uma diferença mínima de anti-aliasing) e foca no que uma pessoa de fato notaria. É a diferença entre um teste que reclama de tudo e um que reclama do que importa. Ferramentas comuns: Percy, Chromatic (para Storybook), Applitools (com checagem por IA) e os snapshots visuais nativos do Playwright (toHaveScreenshot) e do Cypress.

// Snapshot visual nativo no Playwright
await expect(page).toHaveScreenshot('checkout.png', {
  maxDiffPixelRatio: 0.01,        // tolerância
  mask: [page.locator('.data-hoje')] // ignora região dinâmica
})

Onde encaixar (sem encher o pipeline)

Regressão visual é cara: gera imagens, exige armazenamento e pede revisão humana dos diffs. Então não saia "screenshotando" tudo. A estratégia que funciona é mirar nos pontos de maior risco visual — o design system / biblioteca de componentes (onde uma mudança contamina o app inteiro) e um punhado de páginas-chave, em algumas larguras de tela representativas. É a lógica da pirâmide de novo: muitos testes baratos embaixo, poucos testes visuais caros no topo.

No pipeline, o lugar natural é o pull request: a ferramenta posta os diffs como um check, e o merge espera alguém aprovar a mudança visual. Assim a revisão de aparência acontece junto com a revisão de código, e não na reclamação do usuário depois do deploy.

Conclusão

Teste funcional garante que o sistema faz a coisa certa; teste de regressão visual garante que ele parece certo — e os dois falham por motivos diferentes. Capture baselines em ambiente estável, ataque o falso positivo com máscaras, threshold e diff perceptual, e concentre os screenshots no design system e nas páginas críticas. Feito assim, ele pega a classe inteira de bugs que passa despercebida entre "o teste está verde" e "por que o site está assim?".

The E2E test clicks the button, checks that the right text appeared and goes green. Meanwhile, in production, broken CSS pushed that button off the screen, made the text white on a white background or collapsed the whole menu. The test passed because it verifies the DOM, not the appearance. That gap — functional ✓, visual ✗ — is exactly what visual regression testing exists to close.

The blind spot of functional tests

A traditional test asks "does the element exist and have the right content?". It can assert that <button>Comprar</button> is in the DOM and clickable. What it does not see: that the button's color equals its background, that a z-index hid it behind another element, that the font didn't load, or that at 1280px wide the card spilled out of its container. For the user, that's a broken product. For the functional suite, it's all green.

How it works: baseline, comparison and diff

The idea is straightforward: take a picture of the screen and compare it with a previously approved picture. The flow has three steps:

  1. BASELINE      captures the "correct" screen and stores it as a reference
        │
  2. COMPARISON    on every run, takes a new screenshot and compares it
        │          pixel by pixel with the baseline
        ▼
  3. DIFF          ┌─ identical → passes
                   └─ different → fails + image highlighting what changed

When there is a difference, the tool generates a diff image highlighting the changed regions. Then comes the human decision: if the change is a bug, you fix it; if it's an intentional change (you really did redesign the button), you approve the new baseline and it becomes the reference from then on.

The big enemy: the false positive

Comparing pixel by pixel is powerful and naive at the same time. A lot of things change the image without anything being broken — and each one becomes a red test that isn't a bug:

Source of noiseHow to tame it
Font anti-aliasing across OSesGenerate the baseline in the same environment (container/CI), not on your laptop
Dynamic content (dates, names, ads)Mask/ignore the region in the screenshot
Animations and carouselsDisable animations and wait for the screen to settle
Small rendering differencesSet a tolerance threshold (e.g. 0.1%)
Visual testing without false-positive control dies in two weeks: the team gets tired of approving diffs that aren't bugs and starts clicking "approve all" — the moment the test stops testing. Baseline stability isn't a detail, it's what keeps the test alive.

Pixel by pixel vs. perceptual comparison

Newer tools don't compare raw pixels: they use a perceptual diff, which ignores variations imperceptible to the human eye (like a minimal anti-aliasing difference) and focuses on what a person would actually notice. It's the difference between a test that complains about everything and one that complains about what matters. Common tools: Percy, Chromatic (for Storybook), Applitools (with AI-based checking) and the native visual snapshots of Playwright (toHaveScreenshot) and Cypress.

// Native visual snapshot in Playwright
await expect(page).toHaveScreenshot('checkout.png', {
  maxDiffPixelRatio: 0.01,        // tolerance
  mask: [page.locator('.data-hoje')] // ignores a dynamic region
})

Where to fit it (without clogging the pipeline)

Visual regression is expensive: it generates images, requires storage and demands human review of the diffs. So don't go around screenshotting everything. The strategy that works is to aim at the points of highest visual risk — the design system / component library (where one change contaminates the whole app) and a handful of key pages, at a few representative screen widths. It's the pyramid logic again: many cheap tests at the bottom, few expensive visual tests at the top.

In the pipeline, the natural place is the pull request: the tool posts the diffs as a check, and the merge waits for someone to approve the visual change. That way appearance review happens alongside code review, and not in the user's complaint after the deploy.

Conclusion

Functional testing guarantees the system does the right thing; visual regression testing guarantees it looks right — and the two fail for different reasons. Capture baselines in a stable environment, attack false positives with masks, thresholds and perceptual diff, and concentrate screenshots on the design system and critical pages. Done this way, it catches the whole class of bugs that slips through between "the test is green" and "why does the site look like this?".

Leia também

Voltar ao blog