Construção de um Framework de Testes Web Automatizados: Da Estrutura Base à Execução

De Scripts para Casos de Teste

Um conjunto de scripts soltos não é tão eficiente quanto utilizar casos de teste padronizados para gerenciar e executar as validações de forma flexível. Um caso de teste de automação completo deve conter:

  • Setup (preparação): Passos iniciais, métodos auxiliares ou ferramentas que podem ser compartilhados entre os testes.
  • Steps (passos do teste): As etapas principais e exclusivas da validação.
  • Asserções (verificações): Comparação entre o resultado esperado e o real. Um caso pode conter múltiplas asserções.
  • Teardown (limpeza): Ações para restaurar o estado e limpar os efeitos do teste, garantindo que não impactem execuções futuras. Também pode ser compartilhado.

Criando Funções de Teste

Criar um caso de teste Pytest é simples: basta escrever funções que começam com test. Inicialmente, é comum implementar a lógica diretamente no módulo, como no exemplo abaixo, que simula um script inicial rudimentar.

# test_busca_baidu_v0.9.py
from selenium import webdriver
from time import sleep

navegador = webdriver.Chrome()
navegador.get("https://www.baidu.com")
navegador.find_element_by_id('kw').send_keys('Blog do Jardim  Han Zhichao')
navegador.find_element_by_id('su').click()
sleep(1)
if 'Han Zhichao' in navegador.title:
    print('Sucesso')
else:
    print('Falha')
navegador.quit()

O primeiro passo de refatoração é encapsular a lógica em uma função com o padrão Pytest. A verificação é feita com a declaração assert padrão.

# test_busca_baidu_v1.0.py
from selenium import webdriver
from time import sleep

def testar_busca_baidu_01():
    navegador = webdriver.Chrome()
    navegador.get("https://www.baidu.com")
    navegador.find_element_by_id('kw').send_keys('Blog do Jardim  Han Zhichao')
    navegador.find_element_by_id('su').click()
    sleep(1)
    assert 'Han Zhichao' in navegador.title, 'Título não contém Han Zhichao'
    navegador.quit()

Diferente dos scripts Python comuns (executados via python <caminho>), os scripts do Pytest são executados com pytest <caminho> ou python -m pytest <caminho>. Para executar como um script Python padrão, adicione o bloco:

if __name__ == '__main__':
    pytest.main([__file__])

Usando Asserções

Para que a automação seja verdadeiramente útil, é essencial incluir verificações automáticas. A declaração assert do Python é a base para isso. Quando a condição falha, uma exceção AssertionError é lançada, e o Pytest a captura, marcando o caso como falho, sem interromper a execução dos demais testes.

Alternaitvamente, é possível usar pytest.fail() ou lançar manualmente AssertionError dentro de um bloco if:

if 'Han Zhichao' not in navegador.title:
    pytest.fail('Título não contém Han Zhichao')

Estratégias comuns de asserção para Web UI:

  • Fluxo bem-sucedido: Considerar o teste como aprovado se a navegação e as interações forem concluídas sem erros.
  • Título da página: Verificar navegador.title para confirmar a página atual.
  • URL da página: Usar navegador.current_url para validação.
  • Código fonte: Verificar se navegador.page_source contém um texto específico.
  • Presença de elemento: Verificar a existência de um elemento na página.

Exemplo de verificação por elemento:

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

def testar_abrir_baidu():
    navegador = webdriver.Chrome()
    navegador.get("https://www.baidu.com")
    try:
        navegador.find_element_by_id('kw')
    except NoSuchElementException:
        pytest.fail('Campo de busca não encontrado')

No framework, é comum encapsular essas asserções para reuso.

Separando Setup e Teardown

Para melhor organização, isole as etapas de preparação (setup) e limpeza (teardown) das etapas principais do teste. No exemplo anterior, abrir e fechar o navegador são tarefas de setup e teardown.

O Pytest oferece funções setup_function e teardown_function, ou, de forma mais flexível, fixtures personalizadas.

# test_busca_baidu_v3.py
from time import sleep
from selenium import webdriver
import pytest

def setup_function():
    global navegador
    navegador = webdriver.Chrome()

def teardown_function():
    navegador.quit()

def testar_busca_baidu_01():
    navegador.get("https://www.baidu.com")
    navegador.find_element_by_id('kw').send_keys('Blog do Jardim  Han Zhichao')
    navegador.find_element_by_id('su').click()
    sleep(1)
    assert 'Han Zhichao' in navegador.title

if __name__ == '__main__':
    pytest.main([__file__])

Usando Fixtures Personalizadas

A forma mais elegante de gerenciar setup/teardown é com fixtures:

# test_busca_baidu_v4.py
from time import sleep
from selenium import webdriver
import pytest

@pytest.fixture
def navegador():
    drv = webdriver.Chrome()
    yield drv
    drv.quit()

def testar_busca_baidu_01(navegador):
    navegador.get("https://www.baidu.com")
    navegador.find_element_by_id('kw').send_keys('Blog do Jardim  Han Zhichao')
    navegador.find_element_by_id('su').click()
    sleep(1)
    assert 'Han Zhichao' in navegador.title

if __name__ == '__main__':
    pytest.main([__file__, '--html=relatorio.html','--self-contained-html'])

Aqui, navegador é uma fixture. O código antes do yield é o setup; o objeto retornado pelo yield é injetado no teste; e o código após o yield é o teardown.

Usando o Plugin Pytest-Selenium

O ecossistema do Pytest possui diversos plugins que simplificam o trabalho. O pytest-selenium fornece uma fixture global driver (ou selenium) e suporte para alternar entre navegadores. Para usá-lo, instale o plugin e modifique o código:

# test_busca_baidu_v5.py
from time import sleep
import pytest

def testar_busca_baidu_01(driver):
    driver.get("https://www.baidu.com")
    driver.find_element_by_id('kw').send_keys('Blog do Jardim  Han Zhichao')
    driver.find_element_by_id('su').click()
    sleep(1)
    assert 'Han Zhichao' in driver.title

if __name__ == '__main__':
    pytest.main([__file__, '--driver=chrome', '--html=relatorio.html','--self-contained-html'])

O plugin também permite configurar opções do navegador e integração com relatórios HTML (como captura de tela em falhas).

Nota: Por padrão, o pytest-selenium intercepta todas as URLs como sensíveis. Desative isso no pytest.ini com sensitive_url = None se necessário.

Gerando Relatórios de Teste

Os plugins pytest-html e allure-pytest são os mais comuns para gerar relatórios. pytest-html é mais simples e gera um arquivo HTML único. Para usá-lo, instale o pacote e adicione o argumento --html:

if __name__ == '__main__':
    pytest.main([__file__, '--html=relatorio.html', '--self-contained-html'])

Aumentando a Facilidade de Manutenção

A manutenção de testes Web UI é um grande desafio, principalmente devido à volatilidade dos elementos. A melhor prática é usar encapsulamento para isolar as partes que mudam (como localizadores) das partes estáveis (como a lógica de negócio). As principais estratégias são:

  • Código: Isolar localizadores e operações de página.
  • Dados: Manter dados de teste (como senhas) separados do código.
  • Configuração: Usar arquivos de configuração para maior flexibilidade.

Além disso, data-driven testing, logs e captura automática de tela em falhas são ferramentas valiosas para agilizar a depuração.

Captura Automática de Tela em Falhas

Em vez de usar diretamente driver.find_element(), crie uma função encapsulada que captura uma tela se o elemento não for encontrado:

import time
import os
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException

DIRETORIO_CAPTURAS = 'capturas_tela'

def localizar_elemento(driver: webdriver.Chrome, by, value, timeout=5):
    estilo = 'background: green; border: 2px solid red;'
    js = 'arguments[0].setAttribute("style", arguments[1]);'
    try:
        WebDriverWait(driver, timeout).until(
            EC.presence_of_element_located((by, value))
        )
    except TimeoutException:
        arquivo_captura = 'captura_%s.png' % int(time.time())
        driver.save_screenshot(os.path.join(DIRETORIO_CAPTURAS, arquivo_captura))
        raise NoSuchElementException('Elemento %s=%s não encontrado em %s segundos' % (by, value, timeout))
    else:
        elemento = driver.find_element(by, value)
        driver.execute_script(js, elemento, estilo)
    return elemento

Camadas: Encapsulando Etapas de Teste

Utilize uma abordagem em camadas, encapsulando cada operação em funções. Isso permite reutilização e facilita a manutenção. Cada função lida com um elemento específico, centralizando as possíveis alterações.

# test_busca_baidu_v6.py
from time import sleep
import pytest

def localizar_elemento(driver, by, value, timeout=5):
    ...

def abrir_baidu(driver):
    print('Abrindo Baidu')
    driver.get("https://www.baidu.com")

def inserir_palavra_chave(driver, palavra_chave):
    print(f'Inserindo palavra-chave: {palavra_chave}')
    localizar_elemento(driver, 'id', 'kw').send_keys(palavra_chave)

def clicar_botao_busca(driver):
    print('Clicando no botão de busca')
    localizar_elemento(driver, 'id', 'su').click()

def testar_busca_baidu_01(driver):
    abrir_baidu(driver)
    inserir_palavra_chave(driver, 'Blog do Jardim  Han Zhichao')
    clicar_botao_busca(driver)
    sleep(1)
    assert 'Han Zhichao' in driver.title

if __name__ == '__main__':
    pytest.main([__file__, '--html=relatorio.html', '--self-contained-html'])

Separando Dados de Teste

Armazene dados externamente (JSON, YAML) para facilitar a troca entre ambientes.

# dados.json
{
  "palavras_chave": ["Blog do Jardim  Han Zhichao", "Vale Profundo", "Jianshu  Han Zhichao"]
}

# test_busca_baidu_v7.py
import json
import yaml
import pytest

def carregar_json(caminho_arquivo):
    with open(caminho_arquivo) as f:
        return json.load(f)

def carregar_yaml(caminho_arquivo):
    with open(caminho_arquivo) as f:
        return yaml.safe_load(f)

@pytest.fixture
def dados_caso():
    return carregar_yaml('dados.yaml')

def testar_busca_baidu_01(driver, dados_caso):
    palavra_chave = dados_caso['palavras_chave'][0]
    driver.get("https://www.baidu.com")
    driver.find_element_by_id('kw').send_keys(palavra_chave)
    driver.find_element_by_id('su').click()
    sleep(1)
    assert 'Han Zhichao' in driver.title

Usando Data-Driven Testing

Com @pytest.mark.parametrize, execute o mesmo teste com múltiplos conjuntos de dados:

LISTA_PALAVRAS_CHAVE = carregar_yaml('dados.yaml')['palavras_chave']

@pytest.mark.parametrize('palavra_chave', LISTA_PALAVRAS_CHAVE)
def testar_busca_baidu_01(driver, palavra_chave):
    driver.get("https://www.baidu.com")
    driver.find_element_by_id('kw').send_keys(palavra_chave)
    driver.find_element_by_id('su').click()
    sleep(1)
    assert palavra_chave in driver.title

Usando Logs

Para saída mais estruturada, prefira o módulo logging ao print. Configure os níveis de log no pytest.ini:

# pytest.ini
[pytest]
log_cli=True
log_cli_level=INFO
log_cli_format=%(asctime)s %(levelname)s %(message)s
log_cli_date_format=%Y-%m-%d %H:%M:%S

Exemplo de uso:

import logging

def testar_logging():
    logging.info('Informação do passo')
    logging.warning('Aviso')
    logging.error('Erro')
    try:
        assert 0
    except Exception as ex:
        logging.exception(ex)

Dependências entre Casos de Teste

Evite dependências entre testes. Cada caso deve ser independente. Se houver passos sequenciais, como adicionar, consultar e deletar um cliente, a melhor prática é encapsular os passos em funções e reutilizá-los:

def adicionar_cliente():
    pass

def consultar_cliente():
    pass

def deletar_cliente():
    pass

def testar_adicionar_cliente():
    adicionar_cliente()

def testar_consultar_cliente():
    adicionar_cliente()
    consultar_cliente()

def testar_deletar_cliente():
    adicionar_cliente()
    deletar_cliente()

Para casos onde a ordem é estritamente necessária, use o plugin pytest-ordering:

@pytest.mark.run(order=1)
def testar_adicionar_cliente():
    pass

@pytest.mark.run(order=2)
def testar_consultar_cliente():
    pass

Aumentando a Flexibilidade

Alternando entre Ambientes

Use os plugins pytest-base-url e pytest-variables para executar o mesmo conjunto de testes em diferentes ambientes, com diferentes URLs e dados.

from time import sleep
import pytest

def testar_busca_baidu_01(driver, base_url, variables):
    url = base_url + '/'
    palavra_chave = variables['palavras_chave'][0]
    driver.get(url)
    driver.find_element_by_id('kw').send_keys(palavra_chave)
    driver.find_element_by_id('su').click()
    sleep(1)
    assert 'Han Zhichao' in driver.title

Execução:

pytest --driver=chrome --base-url=https://www.baidu.com --variables=dados_teste.json

Marcando Casos de Teste

Use marcadores (@pytest.mark.smoke, @pytest.mark.flaky) para organizar e executar subconjuntos de testes. Registre os marcadores no pytest.ini para evitar erros:

[pytest]
markers =
    smoke: teste de fumaça
    flaky: teste instável
    h5: teste relacionado a H5

Execução: pytest -m "smoke and not flaky"

Tratamento de Casos Instáveis (Flaky Tests)

Estratégias comuns:

  • Pular: Use @pytest.mark.skip ou pytest.skip().
  • Timeout: Instale pytest-timeout e configure um limite de tempo global ou por teste.
  • Reexecução em falha: Use pytest-rerunfailures para reexecutar testes com falha um número específico de vezes.
pytest --reruns 3 --rerun-delay 1

Da Abordagem Estruturada para a Orientada a Objetos

Agrupando Operações por Página (Page Object Model)

O Page Object Model (POM) organiza a automação em torno das páginas da aplicação, encapsulando localizadores e operações. Crie uma classe para cada página:

from time import sleep
from selenium.webdriver.support import expected_conditions as EC

class PaginaInicialBaidu:
    url = 'https://www.baidu.com'
    localizador_campo_busca = ('id', 'kw')
    localizador_botao_busca = ('id', 'su')

    def __init__(self, driver):
        self.driver = driver

    def abrir(self):
        self.driver.get(self.url)

    def inserir_palavra_chave(self, palavra_chave):
        self.driver.find_element(*self.localizador_campo_busca).send_keys(palavra_chave)

    def clicar_botao_buscar(self):
        self.driver.find_element(*self.localizador_botao_busca).click()

    def buscar(self, palavra_chave):
        self.abrir()
        self.inserir_palavra_chave(palavra_chave)
        self.clicar_botao_buscar()
        sleep(0.5)

Encapsulando Métodos Comuns em uma Classe Base

Crie uma classe base para reduzir repetição e adicionar funcionalidades como esperas, manipulação de erros e suporte a elementos esporádicos:

from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import NoSuchElementException, NoAlertPresentException

class PaginaBase:
    url = None

    def __init__(self, driver):
        self.driver = driver

    @property
    def titulo(self):
        return self.driver.title

    def abrir(self, url=None):
        self.driver.get(url or self.url)
        return self

    def localizar_elemento(self, by, value, timeout=None, ignorar_erro=False):
        try:
            if timeout is None:
                return self.driver.find_element(by, value)
            else:
                return WebDriverWait(self.driver, timeout).until(
                    EC.presence_of_element_located((by, value))
                )
        except NoSuchElementException:
            if not ignorar_erro:
                raise

    def clicar_elemento(self, by, value, timeout=None, ignorar_erro=False):
        self.localizar_elemento(by, value, timeout, ignorar_erro).click()
        return self

    def inserir_texto(self, by, value, texto, timeout=None):
        elemento = self.localizar_elemento(by, value, timeout)
        elemento.clear()
        elemento.send_keys(texto)
        return self

    def mover_para_elemento(self, by, value):
        elemento = self.localizar_elemento(by, value)
        ActionChains(self.driver).move_to_element(elemento).perform()
        return self

Com a classe base, a página do Baidu pode ser simplificada:

class PaginaInicialBaidu(PaginaBase):
    url = 'https://www.baidu.com'
    localizador_campo_busca = ('id', 'kw')
    localizador_botao_busca = ('id', 'su')

    def buscar(self, palavra_chave):
        return self.abrir() \
               .inserir_texto(*self.localizador_campo_busca, palavra_chave) \
               .clicar_elemento(*self.localizador_botao_busca) \
               .esperar(0.5)

Melhorando a Eficiência

Modo Headless

Execute testes em modo headless para maior velocidade. Configure a opção no conftest.py:

import pytest

def pytest_addoption(parser):
    parser.addoption('--headless', action='store_true', help='executar chrome em modo headless')

@pytest.fixture
def chrome_options(request, chrome_options):
    if request.config.getoption('--headless'):
        chrome_options.add_argument('--headless')
    return chrome_options

Uso: pytest --headless

Execução Paralela

Distribua casos entre múltiplos processos com pytest-xdist:

pytest -n 4

Envio de E-mail

Adicione uma opção de linha de comando e use hooks para enviar o relatório por e-mail. Configure no conftest.py:

def pytest_addoption(parser):
    parser.addoption("--send-email", action="store_true", help="enviar relatório por e-mail")
    parser.addini('email_subject', help='assunto do e-mail')
    parser.addini('email_receivers', help='destinatários')
    parser.addini('email_body', help='corpo do e-mail')

def pytest_terminal_summary(config):
    enviar_email = config.getoption("--send-email")
    destinatarios = config.getini('email_receivers').split(',')
    if enviar_email and destinatarios:
        caminho_relatorio = config.getoption('htmlpath')
        assunto = config.getini('email_subject')
        corpo = config.getini('email_body')
        # Lógica de envio

Estrutura do Framework

Organize o projeto com diretórios claros:

ProjetoWebAuto/
  ├── dados/
  │   ├── teste.json
  │   └── producao.json
  ├── paginas/
  │   ├── pagina_base.py
  │   └── pagina_baidu.py
  ├── relatorios/
  ├── testes/
  │   └── testar_busca_baidu.py
  ├── utils/
  │   └── enviar_email.py
  ├── conftest.py
  ├── pytest.ini
  ├── requirements.txt
  └── README.md

Tratamento de Dados Sensíveis

Use variáveis de ambiente para senhas e credenciais:

import os
usuario = os.getenv('USUARIO_PADRAO_AUTO')
senha = os.getenv('SENHA_AUTO')

Declarando Dependências

Liste todas as dependências em requirements.txt:

selenium
pytest
pytest-selenium
pytest-html
pytest-variables
pytest-timeout
pytest-level
pytest-base-url
pytest-ordering
pytest-rerunfailures
pytest-xdist

Documentação

Inclua um arquivo README.md explicando a estrutura, as funcionalidades, como escrevre e executar os testes, usando Markdown.

Tags: Selenium pytest Test-Automation web-testing page-object-model

Publicado em 6-21 23:00