Playwright com Python: testes end-to-end modernos
Aprenda a usar Playwright com Python para testar fluxos web reais: instalação, primeiro teste, seletores, fixtures, CI, screenshots e boas práticas.
Playwright virou uma das ferramentas mais fortes para testar aplicações web modernas com Python. Ele controla navegadores reais, espera elementos de forma inteligente, captura screenshots e traces, roda em CI e reduz bastante a fragilidade típica de testes end-to-end escritos com esperas fixas.
Se você já usa pytest para testes unitários e de integração, o próximo passo natural é validar fluxos completos: login, cadastro, checkout, busca, formulários, permissões e páginas que dependem de JavaScript. Este guia mostra como começar com Playwright em Python sem transformar sua suíte em um bloco lento e instável.
Quando usar Playwright
Use Playwright quando o comportamento importante depende do navegador. Exemplos bons:
- validar um formulário com campos obrigatórios;
- testar login, logout e sessão;
- garantir que uma busca retorna resultados;
- conferir um fluxo de compra ou assinatura;
- reproduzir bugs que só aparecem no frontend;
- testar páginas renderizadas por React, Vue, Svelte ou HTMX;
- capturar screenshot ou trace quando algo falha no CI.
Não use Playwright para tudo. Regras de negócio, funções puras e validações de backend continuam mais baratas em testes unitários. A combinação saudável é: muitos testes rápidos, alguns testes de integração e poucos testes end-to-end cobrindo caminhos críticos.
Instalação
Crie ou ative um ambiente virtual e instale o pacote do Playwright com o plugin de pytest:
python -m venv .venv
source .venv/bin/activate
pip install pytest pytest-playwright
playwright install
O comando playwright install baixa os navegadores usados nos testes. Em CI, muitas equipes instalam apenas o Chromium para economizar tempo:
playwright install chromium
Se o projeto usa pyproject.toml, você pode registrar dependências de desenvolvimento:
[project.optional-dependencies]
dev = [
"pytest",
"pytest-playwright",
]
Primeiro teste com pytest
O plugin pytest-playwright fornece fixtures prontas, como page, browser, context e browser_name. O teste abaixo abre uma página, confere o título e valida um texto visível:
# tests/e2e/test_home.py
from re import compile
def test_home_tem_titulo(page):
page.goto("https://example.com")
assert page.title() == "Example Domain"
assert page.get_by_role("heading", name="Example Domain").is_visible()
assert page.get_by_text(compile("illustrative examples", compile.IGNORECASE)).is_visible()
Rode com:
pytest tests/e2e --browser chromium
Para ver o navegador abrindo durante o desenvolvimento:
pytest tests/e2e --headed --slowmo 200
Use --headed só localmente. No CI, mantenha headless para ser mais rápido.
Testando uma aplicação local
Em projetos reais, normalmente você sobe o servidor local antes dos testes. Uma forma simples é deixar a URL base em variável de ambiente:
# tests/e2e/test_login.py
import os
BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000")
def test_login_com_sucesso(page):
page.goto(f"{BASE_URL}/login/")
page.get_by_label("Email").fill("[email protected]")
page.get_by_label("Senha").fill("senha-segura")
page.get_by_role("button", name="Entrar").click()
page.get_by_role("heading", name="Painel").wait_for()
assert page.get_by_text("Bem-vinda, Ana").is_visible()
Esse estilo é mais legível do que selecionar elementos por CSS frágil. Quando possível, prefira seletores que simulam como pessoas e leitores de tela enxergam a interface:
get_by_role()para botões, links, headings, caixas de texto;get_by_label()para campos de formulário;get_by_text()para textos visíveis;get_by_test_id()para componentes sem rótulo natural.
Configure test ids com cuidado
Às vezes não existe um rótulo bom. Nesse caso, adicionar um atributo de teste pode ser melhor do que depender de classes CSS:
<button data-testid="salvar-perfil">Salvar</button>
No teste:
def test_salvar_perfil(page):
page.goto("/perfil/")
page.get_by_test_id("salvar-perfil").click()
assert page.get_by_text("Perfil atualizado").is_visible()
Não exagere. Test ids devem cobrir pontos difíceis de selecionar, não substituir acessibilidade. Se um botão importante não tem nome acessível, o teste está mostrando um problema real da interface.
Fixtures para login e estado compartilhado
Repetir login em todo teste deixa a suíte lenta. Uma alternativa é criar uma fixture que autentica uma vez e salva o estado do navegador:
# tests/e2e/conftest.py
import os
from pathlib import Path
import pytest
BASE_URL = os.environ.get("BASE_URL", "http://localhost:8000")
STATE_FILE = Path(".playwright/auth-state.json")
@pytest.fixture(scope="session")
def auth_state(browser):
STATE_FILE.parent.mkdir(exist_ok=True)
context = browser.new_context()
page = context.new_page()
page.goto(f"{BASE_URL}/login/")
page.get_by_label("Email").fill(os.environ["E2E_USER_EMAIL"])
page.get_by_label("Senha").fill(os.environ["E2E_USER_PASSWORD"])
page.get_by_role("button", name="Entrar").click()
page.get_by_role("heading", name="Painel").wait_for()
context.storage_state(path=str(STATE_FILE))
context.close()
return str(STATE_FILE)
@pytest.fixture
def authenticated_page(browser, auth_state):
context = browser.new_context(storage_state=auth_state)
page = context.new_page()
yield page
context.close()
Agora os testes usam authenticated_page:
def test_usuario_ve_pedidos(authenticated_page):
page = authenticated_page
page.goto("/pedidos/")
assert page.get_by_role("heading", name="Pedidos").is_visible()
Nunca coloque senha real no repositório. Use variáveis de ambiente no CI e contas de teste com permissões limitadas.
Esperas automáticas e flakiness
Um erro comum em Selenium é usar sleep() para esperar a página. Playwright já espera muitas condições automaticamente: o elemento existir, ficar visível, estar habilitado e poder receber clique.
Ruim:
import time
def test_busca(page):
page.goto("/busca/")
page.get_by_label("Buscar").fill("django")
page.get_by_role("button", name="Buscar").click()
time.sleep(3)
assert page.get_by_text("Django").is_visible()
Melhor:
def test_busca(page):
page.goto("/busca/")
page.get_by_label("Buscar").fill("django")
page.get_by_role("button", name="Buscar").click()
page.get_by_role("heading", name="Resultados").wait_for()
assert page.get_by_text("Django").is_visible()
Se o teste depende de uma chamada de API específica, espere a resposta:
def test_carrega_relatorio(page):
page.goto("/relatorios/")
with page.expect_response("**/api/relatorios/**") as resposta:
page.get_by_role("button", name="Atualizar").click()
assert resposta.value.ok
assert page.get_by_text("Relatório atualizado").is_visible()
Screenshots, vídeos e traces
Quando um teste falha no CI, o melhor artefato é aquele que mostra o que o navegador viu. O pytest-playwright suporta screenshots e traces por flag:
pytest tests/e2e \
--browser chromium \
--screenshot only-on-failure \
--video retain-on-failure \
--tracing retain-on-failure
Para abrir um trace localmente:
playwright show-trace test-results/caminho-do-trace.zip
O trace mostra ações, DOM, console, rede e screenshots por etapa. Isso costuma economizar muito tempo em falhas intermitentes.
Rodando em múltiplos navegadores
Para cobrir compatibilidade, rode a mesma suíte em Chromium, Firefox e WebKit:
pytest tests/e2e --browser chromium --browser firefox --browser webkit
No dia a dia, uma estratégia prática é:
- pull request: Chromium nos fluxos críticos;
- nightly: Chromium, Firefox e WebKit;
- antes de release grande: suíte completa com traces em falhas.
Isso evita que cada PR fique lento demais.
Exemplo de CI no GitHub Actions
Um workflow simples pode instalar dependências, baixar navegador e rodar testes:
name: e2e
on:
pull_request:
push:
branches: [main]
jobs:
playwright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e ".[dev]"
- run: playwright install --with-deps chromium
- run: pytest tests/e2e --browser chromium --tracing retain-on-failure
env:
BASE_URL: http://localhost:8000
E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
Se a aplicação precisa subir antes, adicione um passo com o servidor em background e espere a porta responder:
- run: |
python manage.py migrate
python manage.py runserver 0.0.0.0:8000 &
python - <<'PY'
import time, urllib.request
for _ in range(60):
try:
urllib.request.urlopen("http://localhost:8000/", timeout=1)
break
except Exception:
time.sleep(1)
else:
raise SystemExit("servidor nao iniciou")
PY
Playwright para scraping e automação
Playwright também pode automatizar navegação fora de testes. Para scraping, use com responsabilidade: respeite robots.txt, termos de uso, limites de requisição e dados pessoais.
Exemplo mínimo com a API síncrona:
from playwright.sync_api import sync_playwright
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
print(page.get_by_role("heading").inner_text())
browser.close()
if __name__ == "__main__":
main()
Para páginas estáticas, BeautifulSoup com Requests costuma ser mais rápido e barato. Use Playwright quando você realmente precisa de JavaScript, login, cliques ou estado de navegador.
Comparação rápida: Playwright vs Selenium
Selenium continua útil, maduro e muito conhecido. Playwright, porém, costuma ser mais confortável em projetos novos:
| Critério | Playwright | Selenium |
|---|---|---|
| Esperas automáticas | Muito fortes por padrão | Exige mais configuração explícita |
| Instalação de browsers | Integrada ao CLI | Depende de drivers e versões |
| Trace e debug | Trace viewer excelente | Depende de ferramentas externas |
| API moderna | Simples e consistente | Mais verbosa |
| Ecossistema legado | Menor | Muito grande |
Se você já mantém uma suíte Selenium estável, não precisa migrar tudo de uma vez. Comece usando Playwright em novos fluxos críticos ou em testes que hoje são muito instáveis. Para entender o caminho tradicional, veja também Selenium em Python e automação web com Selenium.
Boas práticas para uma suíte saudável
Uma suíte end-to-end boa é pequena, confiável e focada em risco. Algumas regras ajudam:
- Teste jornadas, não detalhes internos. Prefira “usuário compra produto” a “classe CSS mudou”.
- Use dados descartáveis. Crie usuários, pedidos e registros de teste que possam ser limpos.
- Evite dependência entre testes. Cada teste deve conseguir rodar sozinho.
- Separe unitário, integração e e2e. Use
tests/unit,tests/integrationetests/e2e. - Marque testes lentos. Rode o essencial em PR e o resto em agendamento.
- Capture artefatos em falhas. Screenshot, vídeo e trace tornam o CI debuggável.
- Prefira seletores acessíveis. Eles melhoram o teste e a interface.
- Não use produção para testes destrutivos. Tenha ambiente de staging ou dados fake.
Exemplo de markers no pyproject.toml:
[tool.pytest.ini_options]
markers = [
"e2e: testes end-to-end com navegador",
"smoke: fluxos críticos rápidos",
]
E no teste:
import pytest
@pytest.mark.e2e
@pytest.mark.smoke
def test_checkout_basico(page):
page.goto("/checkout/")
assert page.get_by_role("heading", name="Checkout").is_visible()
Checklist para começar hoje
Se você quer adicionar Playwright a um projeto Python existente, faça o menor passo útil:
- instale
pytest-playwright; - crie
tests/e2e/test_home.py; - valide uma página pública ou tela de login;
- rode no CI apenas com Chromium;
- habilite screenshot e trace em falhas;
- depois adicione um fluxo realmente crítico.
Esse caminho evita a armadilha de tentar automatizar a aplicação inteira de uma vez. Testes end-to-end funcionam melhor quando protegem os fluxos que quebrariam o negócio, enquanto testes unitários com pytest e testes de integração cobrem o restante.
Se você trabalha com múltiplas stacks, vale comparar a abordagem do Playwright em Python com ferramentas de outras linguagens: em Go, testes HTTP e binários simples favorecem suítes rápidas; em Rust, a segurança de tipos reduz classes inteiras de erro antes mesmo do navegador entrar em cena. Em Python, Playwright fecha a lacuna do teste real de interface com uma API produtiva e legível.