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 manualmenteAssertionErrordentro de um blocoif: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.titlepara confirmar a página atual. - URL da página: Usar
navegador.current_urlpara validação. - Código fonte: Verificar se
navegador.page_sourceconté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.skipoupytest.skip(). - Timeout: Instale
pytest-timeoute configure um limite de tempo global ou por teste. - Reexecução em falha: Use
pytest-rerunfailurespara 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.