Além Da Lente: Os Limites Da Automação Visual E A Realidade Das Trincheiras No Legado

PRÓLOGO: QUANDO O ESPELHO REVELA SUAS RACHADURAS
Há algumas semanas, compartilhei nossa jornada de transformar o Selenium em uma máquina do tempo visual — capaz de documentar bugs através de vídeos que contam histórias. Os stakeholders ficaram impressionados. Os desenvolvedores finalmente tinham evidências irrefutáveis. A comunicação melhorou dramaticamente.
Mas conforme a suíte de testes crescia, verdades inconvenientes começaram a emergir das sombras. O que funcionava perfeitamente com 5 bugs documentados começou a ranger com 50. O que era "mágico" para demonstrações passou a ser "pesadelo" no pipeline de CI/CD.
Este artigo é a continuação necessária — não para invalidar o que construímos, mas para refiná-lo. Porque a verdadeira maestria não está em criar ferramentas brilhantes, mas em reconhecer seus limites e transcendê-los.
Vamos falar sobre os furos que descobrimos e, mais importante, como os corrigimos.
CAPÍTULO 1: O CUSTO OCULTO DO "GRAVE TUDO"
No artigo anterior, defendi a gravação contínua de vídeos em 1920x1080 a 30 FPS para cada teste. Era bonito na teoria. Na prática, descobrimos o peso real dessa decisão.
O PROBLEMA: EXPLOSÃO DE ARMAZENAMENTO
Considere os números:
- Cada teste gera um vídeo — aproximadamente 30-60 segundos
- A 30 FPS em Full HD — isso representa ~50-100 MB por teste (com codec H.264)
- Uma suíte de regressão com 200 testes — ~10-20 GB por execução
- Pipeline de CI/CD executando 5 vezes ao dia — ~50-100 GB diários
- Em um mês: 1.5 a 3 TERABYTES apenas em vídeos de testes
E o pior: 95% desses vídeos eram de testes que passaram. Estávamos armazenando terabytes de evidências de que tudo estava funcionando — informação com valor próximo de zero.
O IMPACTO NO PIPELINE
O tempo de execução do CI/CD começou a degradar:
- Gravação consome CPU e I/O — durante o teste
- Upload dos artefatos de vídeo — para o servidor de CI levava minutos
- Jenkins ficava lento — para renderizar relatórios com centenas de vídeos anexados
- Desenvolvedores começaram a ignorar os relatórios — "demora muito para abrir"
A CORREÇÃO: CAPTURA CONDICIONAL
A solução foi elegantemente simples: só salve o vídeo se o teste falhar.
Implementamos o conceito de "gravação fantasma": o vídeo é sempre gravado em um buffer temporário durante a execução, mas só é persistido em disco se o teste terminar com status de falha.
class ConditionalVideoRecorder:
"""
Gravador inteligente: captura sempre, persiste condicionalmente.
"""
def __init__(self, test_name: str, buffer_size_seconds: int = 120):
self.test_name = test_name
self.temp_buffer = tempfile.NamedTemporaryFile(
suffix='.mp4', delete=False
)
self.is_recording = False
self.final_path = None
def start(self):
"""Inicia gravação para buffer temporário."""
self.writer = cv2.VideoWriter(
self.temp_buffer.name,
cv2.VideoWriter_fourcc(*'mp4v'),
30.0,
(1920, 1080)
)
self.is_recording = True
self._capture_loop()
def stop(self, test_passed: bool):
"""
Finaliza gravação.
Se teste PASSOU: descarta o vídeo temporário
Se teste FALHOU: move para diretório de evidências
"""
self.is_recording = False
self.writer.release()
if test_passed:
# Teste passou - descarta evidência
os.unlink(self.temp_buffer.name)
logger.info(f"✓ Teste {self.test_name} passou - vídeo descartado")
else:
# Teste falhou - preserva evidência
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
self.final_path = f"evidencias/{timestamp}_{self.test_name}_FALHA.mp4"
shutil.move(self.temp_buffer.name, self.final_path)
logger.warning(f"✗ Teste {self.test_name} falhou - vídeo salvo: {self.final_path}")
return self.final_path
OS RESULTADOS
Após implementar a captura condicional:
Armazenamento reduziu de ~100 GB/dia para ~2-5 GB/dia — uma economia de mais de 95%.
Tempo de upload de artefatos caiu 90% — o pipeline voltou a ser ágil.
Relatórios do Jenkins carregam instantaneamente — sem centenas de vídeos anexados.
Desenvolvedores voltaram a consultar os relatórios — a ferramenta recuperou sua utilidade.
A lição: evidência abundante não é evidência útil. Menos é mais quando se trata de documentação de falhas.
CAPÍTULO 2: DA ARQUEOLOGIA VISUAL PARA A ARQUITETURA DE TESTES
O artigo anterior focava muito em "ver o bug". Mas há um problema fundamental: se você precisa constantemente ver bugs através de vídeos, significa que seu sistema não está preparado para ser testado de forma sustentável.
O PROBLEMA: TESTES FRÁGEIS NO ANGULAR 1.5
Nossa aplicação legada usava AngularJS 1.5. Os IDs dos elementos eram gerados dinamicamente pelo framework:
<!-- HTML gerado pelo AngularJS -->
<button id="button_ng_1_0_3_7" class="btn btn-primary">Salvar</button>
<input id="input_ng_1_0_3_8" type="text" ng-model="empresa.nome">
<div id="div_ng_1_0_3_9" ng-repeat="item in lista">...</div>
Esses IDs mudavam a cada refresh da página, a cada deploy, às vezes a cada navegação. Nossos testes Selenium estavam cheios de seletores como:
# Testes frágeis - ANTI-PADRÃO
driver.find_element(By.ID, "button_ng_1_0_3_7").click()
driver.find_element(By.XPATH, "//div[@class='modal-body']//button[2]").click()
driver.find_element(By.CSS_SELECTOR, "table > tbody > tr:nth-child(3) > td:nth-child(5) > a").click()
Resultado: testes quebravam constantemente mesmo sem mudanças funcionais. O vídeo mostrava "o botão não foi encontrado", mas a realidade era: o botão estava lá, só com outro ID.
A CORREÇÃO: PAGE OBJECT MODEL + DATA-ATTRIBUTES
"Iluminar o passado" não é apenas filmar o erro — é reformar o HTML legado para torná-lo testável.
Passo 1: Adicionar Data-Attributes ao HTML
Trabalhamos com a equipe de desenvolvimento para adicionar atributos estáveis ao HTML:
<!-- ANTES: IDs instáveis gerados pelo Angular -->
<button id="button_ng_1_0_3_7" class="btn btn-primary">Salvar</button>
<!-- DEPOIS: Data-attributes estáveis para testes -->
<button id="button_ng_1_0_3_7"
class="btn btn-primary"
data-test="btn-salvar-empresa">Salvar</button>
A convenção que adotamos:
data-test="btn-*"para botõesdata-test="input-*"para campos de entradadata-test="link-*"para linksdata-test="table-*"para tabelasdata-test="modal-*"para modais
Passo 2: Implementar Page Object Model
Em vez de espalhar seletores pelo código de teste, centralizamos em classes dedicadas:
class ManterEmpresaPage:
"""
Page Object para o módulo Manter Empresa.
Encapsula todos os seletores e interações com a página.
"""
# Seletores estáveis usando data-test
SELECTORS = {
'btn_nova_empresa': '[data-test="btn-nova-empresa"]',
'btn_salvar': '[data-test="btn-salvar-empresa"]',
'btn_cancelar': '[data-test="btn-cancelar-empresa"]',
'btn_excluir': '[data-test="btn-excluir-empresa"]',
'input_razao_social': '[data-test="input-razao-social"]',
'input_cnpj': '[data-test="input-cnpj"]',
'input_email': '[data-test="input-email"]',
'table_empresas': '[data-test="table-lista-empresas"]',
'modal_confirmacao': '[data-test="modal-confirmacao"]',
}
def __init__(self, driver: WebDriver):
self.driver = driver
self.wait = WebDriverWait(driver, timeout=15)
def navegar(self):
"""Navega para a página de Manter Empresa."""
self.driver.get(f"{BASE_URL}/admin/empresa")
self._aguardar_carregamento()
return self
def _aguardar_carregamento(self):
"""Aguarda Angular terminar de renderizar."""
self.wait.until(
lambda d: d.execute_script(
"return typeof angular !== 'undefined' && "
"angular.element(document.body).injector() && "
"angular.element(document.body).injector().get('$http').pendingRequests.length === 0"
)
)
def clicar_nova_empresa(self):
"""Clica no botão de nova empresa."""
btn = self.wait.until(
EC.element_to_be_clickable(
(By.CSS_SELECTOR, self.SELECTORS['btn_nova_empresa'])
)
)
btn.click()
return self
def preencher_formulario(self, razao_social: str, cnpj: str, email: str):
"""Preenche o formulário de empresa."""
self._preencher_campo('input_razao_social', razao_social)
self._preencher_campo('input_cnpj', cnpj)
self._preencher_campo('input_email', email)
return self
def _preencher_campo(self, selector_key: str, valor: str):
"""Preenche um campo com tratamento de espera."""
campo = self.wait.until(
EC.visibility_of_element_located(
(By.CSS_SELECTOR, self.SELECTORS[selector_key])
)
)
campo.clear()
campo.send_keys(valor)
def salvar(self):
"""Clica em salvar e aguarda processamento."""
btn = self.wait.until(
EC.element_to_be_clickable(
(By.CSS_SELECTOR, self.SELECTORS['btn_salvar'])
)
)
btn.click()
self._aguardar_carregamento()
return self
def obter_empresas_listadas(self) -> List[Dict]:
"""Retorna lista de empresas da tabela."""
self._aguardar_carregamento()
table = self.driver.find_element(
By.CSS_SELECTOR, self.SELECTORS['table_empresas']
)
rows = table.find_elements(By.TAG_NAME, 'tr')
empresas = []
for row in rows[1:]: # Pula header
cols = row.find_elements(By.TAG_NAME, 'td')
if len(cols) >= 3:
empresas.append({
'razao_social': cols[0].text,
'cnpj': cols[1].text,
'email': cols[2].text
})
return empresas
Passo 3: Testes Limpos e Legíveis
Com o Page Object, os testes ficam declarativos e fáceis de manter:
class TestManterEmpresa:
"""
Testes do módulo Manter Empresa usando Page Object Model.
"""
def setup_method(self):
self.driver = webdriver.Chrome()
self.page = ManterEmpresaPage(self.driver)
self.recorder = ConditionalVideoRecorder("ManterEmpresa")
self.recorder.start()
def teardown_method(self):
test_passed = not hasattr(self, '_outcome') or \
self._outcome.success
self.recorder.stop(test_passed)
self.driver.quit()
def test_criar_empresa_com_sucesso(self):
"""
DADO que estou na página de Manter Empresa
QUANDO preencho os dados válidos e salvo
ENTÃO a empresa deve aparecer na listagem
"""
# Arrange
dados_empresa = {
'razao_social': 'EMPRESA TESTE AUTOMATIZADO LTDA',
'cnpj': '12.345.678/0001-99',
'email': 'teste@empresa.com.br'
}
# Act
self.page.navegar() \
.clicar_nova_empresa() \
.preencher_formulario(**dados_empresa) \
.salvar()
# Assert
empresas = self.page.obter_empresas_listadas()
assert any(
e['razao_social'] == dados_empresa['razao_social']
for e in empresas
), f"Empresa não encontrada na listagem: {dados_empresa['razao_social']}"
A DIFERENÇA NA MANUTENIBILIDADE
Comparação de cenários de mudança:
| Cenário de Mudança | Sem Page Object | Com Page Object |
|---|---|---|
| Botão muda de ID | Alterar 47 arquivos de teste | Alterar 1 linha no Page Object |
| Novo campo no formulário | Modificar cada teste que usa o form | Adicionar método no Page Object |
| Modal redesenhado | Caçar todos os XPaths do modal | Atualizar seletores centralizados |
CAPÍTULO 3: LOGS vs. PIXELS — A VERDADEIRA CAUSA RAIZ
Aqui está uma verdade desconfortável: o desenvolvedor não conserta o código olhando um vídeo MP4. Ele conserta olhando a stacktrace.
O PROBLEMA: O VÍDEO MOSTRA O SINTOMA, NÃO A CAUSA
Considere o BUG-003 que documentamos no artigo anterior. O vídeo mostrava:
- Usuário preenche formulário ✓
- Usuário clica em Salvar ✓
- Sistema retorna para listagem ✓
- Registro NÃO aparece ✗
Perfeito para comunicar o problema ao stakeholder. Inútil para o desenvolvedor. Onde está a exceção do Hibernate? Qual foi o erro de transação? Qual stored procedure falhou?
O vídeo grita "algo deu errado", mas sussurra sobre "o que exatamente".
A CORREÇÃO: CORRELAÇÃO DE LOGS
Implementamos um sistema de rastreamento que conecta o teste Selenium aos logs do servidor WildFly/JBoss.
Passo 1: Injetar Correlation ID nas Requisições
Cada teste gera um ID único que viaja junto com todas as requisições HTTP:
class CorrelatedSeleniumTest:
"""
Base class para testes com correlação de logs.
"""
def setup_method(self):
self.correlation_id = f"TEST-{uuid.uuid4().hex[:12]}"
self.driver = webdriver.Chrome()
# Injetar correlation ID via JavaScript para todas as requisições
self._inject_correlation_interceptor()
def _inject_correlation_interceptor(self):
"""
Injeta interceptor que adiciona header X-Correlation-ID
em todas as requisições AJAX do AngularJS.
"""
script = f"""
(function() {{
var originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {{
var result = originalXHROpen.apply(this, arguments);
this.setRequestHeader('X-Correlation-ID', '{self.correlation_id}');
return result;
}};
// Para AngularJS $http
if (typeof angular !== 'undefined') {{
angular.module('app').config(['$httpProvider', function($httpProvider) {{
$httpProvider.defaults.headers.common['X-Correlation-ID'] = '{self.correlation_id}';
}}]);
}}
}})();
"""
self.driver.execute_script(script)
def get_server_logs_for_test(self) -> List[str]:
"""
Busca logs do servidor WildFly filtrados pelo correlation ID.
"""
log_path = "/var/log/wildfly/server.log" # ou via API de monitoramento
logs = []
# Em produção, usar ELK Stack, Splunk, ou similar
with open(log_path, 'r') as f:
for line in f:
if self.correlation_id in line:
logs.append(line.strip())
return logs
Passo 2: Configurar Logback no Java EE para Capturar o ID
No backend WildFly, configuramos o MDC (Mapped Diagnostic Context) do Logback:
// Filtro JAX-RS que extrai o Correlation ID
@Provider
@Priority(Priorities.HEADER_DECORATOR)
public class CorrelationIdFilter implements ContainerRequestFilter, ContainerResponseFilter {
private static final String CORRELATION_HEADER = "X-Correlation-ID";
private static final String MDC_KEY = "correlationId";
@Override
public void filter(ContainerRequestContext requestContext) {
String correlationId = requestContext.getHeaderString(CORRELATION_HEADER);
if (correlationId == null || correlationId.isEmpty()) {
correlationId = "REQ-" + UUID.randomUUID().toString().substring(0, 12);
}
// Adiciona ao MDC para aparecer em todos os logs desta thread
MDC.put(MDC_KEY, correlationId);
// Propaga para resposta
requestContext.setProperty(CORRELATION_HEADER, correlationId);
}
@Override
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext) {
String correlationId = (String) requestContext.getProperty(CORRELATION_HEADER);
if (correlationId != null) {
responseContext.getHeaders().add(CORRELATION_HEADER, correlationId);
}
MDC.remove(MDC_KEY);
}
}
Configuração do logback.xml:
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/wildfly/server.log</file>
<encoder>
<!-- Inclui correlationId em cada linha de log -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{correlationId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</configuration>
Passo 3: Relatório Unificado com Vídeo + Logs
No teardown do teste, geramos um relatório que correlaciona tudo:
def teardown_method(self):
test_passed = self._check_test_outcome()
video_path = self.recorder.stop(test_passed)
if not test_passed:
# Gerar relatório de falha com correlação
self._generate_failure_report(video_path)
def _generate_failure_report(self, video_path: str):
"""
Gera relatório HTML com vídeo + logs correlacionados.
"""
server_logs = self.get_server_logs_for_test()
# Identificar exceções e erros nos logs
exceptions = [log for log in server_logs if 'Exception' in log or 'ERROR' in log]
report = f"""
<html>
<head><title>Relatório de Falha - {self.test_name}</title></head>
<body>
<h1>Relatório de Falha: {self.test_name}</h1>
<h2>Correlation ID: {self.correlation_id}</h2>
<h2>Evidência Visual</h2>
<video controls width="100%">
<source src="{video_path}" type="video/mp4">
</video>
<h2>Logs do Servidor (filtrados)</h2>
<h3>Exceções Encontradas ({len(exceptions)})</h3>
<pre style="background: #1e1e1e; color: #ff6b6b; padding: 15px;">
{chr(10).join(exceptions)}
</pre>
<h3>Log Completo da Requisição</h3>
<pre style="background: #1e1e1e; color: #d4d4d4; padding: 15px;">
{chr(10).join(server_logs)}
</pre>
<h2>Timeline</h2>
<p>Use o timestamp do vídeo para correlacionar com os logs acima.</p>
</body>
</html>
"""
report_path = f"evidencias/{self.correlation_id}_REPORT.html"
with open(report_path, 'w') as f:
f.write(report)
O RESULTADO: DIAGNÓSTICO EM MINUTOS, NÃO HORAS
Agora, quando um teste falha, o desenvolvedor recebe:
Vídeo mostrando o comportamento na UI — para contexto visual do que aconteceu.
Correlation ID para rastrear a requisição específica — o identificador único que conecta frontend e backend.
Logs do servidor filtrados — todas as mensagens daquela requisição específica, sem ruído.
Exceções destacadas — Hibernate, JPA, SQL e outros erros críticos em evidência.
O tempo médio de diagnóstico caiu de 2-3 horas para 15-30 minutos.
CAPÍTULO 4: O FIM DOS "TESTES DE NECROTÉRIO"
Um anti-padrão sutil emergiu conforme documentávamos mais bugs: a proliferação de testes isolados.
O PROBLEMA: SUÍTE VIRA ARQUIVO MORTO
Para cada bug encontrado, criávamos um teste específico:
03_casos-teste/
└── funcional/
└── bug/
├── test_bug001_login_falha_campo_vazio.py
├── test_bug002_empresa_timeout_save.py
├── test_bug003_empresa_crud_nao_persiste.py
├── test_bug004_relatorio_encoding_errado.py
├── test_bug005_modal_nao_fecha.py
├── test_bug006_data_formato_invalido.py
├── test_bug007_paginacao_quebra.py
... (47 arquivos depois)
└── test_bug047_upload_arquivo_grande.py
Cada teste era uma "prova" de que um bug existia e foi corrigido. Mas:
A suíte de regressão levava 4 horas para executar — tempo inaceitável para feedback rápido.
Muitos testes testavam a mesma funcionalidade — de formas ligeiramente diferentes, criando redundância.
Novos desenvolvedores ficavam perdidos — não sabiam quais testes eram críticos vs. quais eram "memoriais históricos".
Manutenção virou pesadelo — cada mudança na UI quebrava dezenas de testes de bug.
Estávamos criando um necrotério de testes: cada bug morto tinha seu próprio "túmulo" que precisávamos manter eternamente.
A CORREÇÃO: SUÍTE DE REGRESSÃO VIVA
A solução foi uma mudança de mentalidade: quando um bug é corrigido, o teste que o encontrou deve ser absorvido pelo teste funcional principal, não mantido como entidade separada.
Antes: Testes de Bug Isolados
# test_bug003_empresa_crud_nao_persiste.py
class TestBug003:
"""BUG-003: CRUD de empresa não persiste dados."""
def test_criar_empresa_persiste(self):
# Teste específico para o bug 003
pass
# test_bug012_empresa_validacao_cnpj.py
class TestBug012:
"""BUG-012: CNPJ inválido não mostra erro."""
def test_cnpj_invalido_mostra_erro(self):
# Teste específico para o bug 012
pass
Depois: Testes Funcionais Consolidados
# test_manter_empresa.py
class TestManterEmpresa:
"""
Testes funcionais completos do módulo Manter Empresa.
Histórico de Bugs Incorporados:
- BUG-003 (2026-01-15): CRUD não persistia - test_criar_empresa_persiste
- BUG-012 (2026-01-22): Validação CNPJ faltando - test_criar_empresa_cnpj_invalido
- BUG-019 (2026-02-01): Timeout em save - coberto por test_criar_empresa_persiste
"""
# === CENÁRIOS DE SUCESSO ===
def test_criar_empresa_persiste(self):
"""
Verifica criação completa de empresa com dados válidos.
Garante: Persistência funciona (BUG-003), timeout não ocorre (BUG-019).
"""
# Implementação
pass
def test_editar_empresa_atualiza_dados(self):
"""Verifica edição de empresa existente."""
pass
def test_excluir_empresa_remove_registro(self):
"""Verifica exclusão de empresa."""
pass
# === CENÁRIOS DE VALIDAÇÃO ===
def test_criar_empresa_cnpj_invalido_mostra_erro(self):
"""
Verifica que CNPJ inválido exibe mensagem de erro.
Garante: Validação frontend funciona (BUG-012).
"""
pass
def test_criar_empresa_campos_obrigatorios_vazios(self):
"""Verifica validação de campos obrigatórios."""
pass
# === CENÁRIOS DE BORDA ===
def test_criar_empresa_razao_social_max_length(self):
"""Verifica comportamento com razão social muito longa."""
pass
O Processo de Absorção
Quando um bug é corrigido:
- Validar a correção — com o teste de bug existente
- Identificar o teste funcional — que deveria cobrir esse cenário
- Adicionar o cenário ao teste funcional — ou criar se não existir
- Documentar no docstring — qual bug aquele teste previne
- Arquivar o teste de bug original — não deletar, mover para pasta de histórico
- Atualizar o card do bug — com referência ao teste funcional
A ESTRUTURA REFATORADA
03_casos-teste/
├── funcional/
│ ├── test_login.py # Todos cenários de login
│ ├── test_manter_empresa.py # Todos cenários de empresa
│ ├── test_manter_usuario.py # Todos cenários de usuário
│ ├── test_relatorios.py # Todos cenários de relatório
│ └── test_configuracoes.py # Todos cenários de config
├── integracao/
│ └── test_api_endpoints.py
├── regressao/
│ └── test_smoke.py # Testes críticos rápidos
└── _historico_bugs/ # Arquivo morto - apenas referência
├── test_bug001_ARQUIVADO.py
├── test_bug002_ARQUIVADO.py
└── ...
OS BENEFÍCIOS
Suíte de regressão reduziu de 4 horas para 45 minutos — feedback muito mais rápido.
Número de arquivos de teste caiu de 73 para 12 — manutenção simplificada.
Novos desenvolvedores entendem a cobertura em minutos — onboarding facilitado.
Mudanças na UI afetam menos arquivos — menos retrabalho em testes.
Cada teste conta uma história completa da funcionalidade — documentação viva do sistema.
CAPÍTULO 5: WebDriverWait vs. pyautogui — A ESCOLHA CERTA
No artigo anterior, mencionei o uso de pyautogui para algumas interações. Vou ser honesto: foi um erro que precisou ser corrigido.
O PROBLEMA: pyautogui É FRÁGIL EM TESTES WEB
pyautogui interage no nível do sistema operacional — coordenadas de pixel, não elementos DOM. Em testes web, isso significa:
Resolução diferente = teste quebra — o pixel 500,300 muda de lugar.
Zoom do browser diferente = teste quebra — a escala altera tudo.
Posição da janela diferente = teste quebra — coordenadas absolutas falham.
Qualquer popup do SO = teste quebra — foco vai para outro lugar.
A CORREÇÃO: WebDriverWait para TUDO
Substituímos todas as interações pyautogui por WebDriverWait explícito:
# ANTES: pyautogui (frágil)
import pyautogui
pyautogui.click(500, 300) # Clica em coordenada fixa
pyautogui.typewrite('texto')
time.sleep(3) # Espera fixa
# DEPOIS: WebDriverWait (robusto)
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, timeout=15)
# Clica em elemento, não em coordenada
elemento = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-test="btn-salvar"]')))
elemento.click()
# Digita em campo, não em "tela"
campo = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, '[data-test="input-nome"]')))
campo.send_keys('texto')
# Espera condição, não tempo fixo
wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, '.loading-spinner')))
CONDIÇÕES DE ESPERA CUSTOMIZADAS PARA ANGULARJS
Para AngularJS 1.5, criamos condições de espera específicas:
class AngularConditions:
"""Condições de espera customizadas para AngularJS 1.5."""
@staticmethod
def angular_ready():
"""Aguarda Angular terminar processamento."""
def _predicate(driver):
try:
return driver.execute_script("""
if (typeof angular === 'undefined') {
return true; // Não é página Angular
}
var injector = angular.element(document.body).injector();
if (!injector) {
return false; // Injector ainda não existe
}
var $http = injector.get('$http');
var $timeout = injector.get('$timeout');
return $http.pendingRequests.length === 0;
""")
except:
return False
return _predicate
@staticmethod
def ng_repeat_loaded(selector: str, min_items: int = 1):
"""Aguarda ng-repeat popular lista."""
def _predicate(driver):
try:
items = driver.find_elements(By.CSS_SELECTOR, selector)
return len(items) >= min_items
except:
return False
return _predicate
@staticmethod
def modal_opened(modal_selector: str):
"""Aguarda modal Bootstrap/Angular abrir completamente."""
def _predicate(driver):
try:
modal = driver.find_element(By.CSS_SELECTOR, modal_selector)
# Verifica se modal está visível E a animação terminou
is_visible = modal.is_displayed()
has_animation = 'fade' in modal.get_attribute('class')
is_animated = 'in' in modal.get_attribute('class') if has_animation else True
return is_visible and is_animated
except:
return False
return _predicate
# Uso
wait = WebDriverWait(driver, 15)
wait.until(AngularConditions.angular_ready())
wait.until(AngularConditions.ng_repeat_loaded('[data-test="empresa-row"]', min_items=1))
wait.until(AngularConditions.modal_opened('[data-test="modal-confirmacao"]'))
CAPÍTULO 6: O SELENIUM NÃO SUBSTITUI O TESTE DE INTEGRAÇÃO
Uma tentação que enfrentamos foi confiar apenas no Selenium para validar o sistema. Se o teste E2E passa, está tudo certo, não?
Errado.
O PROBLEMA: SELENIUM TESTA A PONTA, NÃO O MEIO
O teste Selenium valida que:
O usuário consegue ver a tela ✓
O usuário consegue clicar nos botões ✓
O sistema responde de alguma forma ✓
Mas NÃO valida:
A lógica de negócio está correta? — Selenium não sabe.
As queries SQL estão otimizadas? — Selenium não vê o banco.
O cache está funcionando? — Selenium não monitora infraestrutura.
A transação está commitando corretamente? — Selenium só vê o resultado final.
Os eventos estão sendo publicados? — Selenium não escuta filas.
Os logs estão sendo gerados corretamente? — Selenium não lê arquivos de log.
A CORREÇÃO: PIRÂMIDE DE TESTES COMPLETA
Implementamos uma estrutura de testes em camadas:
/\
/ \
/ E2E \ <-- Selenium (poucos, lentos, caros)
/ (10) \
/----------\
/ \
/ Integração \ <-- JUnit + Arquillian (médios)
/ (50) \
/------------------\
/ \
/ Unitários \ <-- JUnit + Mockito (muitos, rápidos)
/ (200) \
/==========================\
Testes Unitários (Java EE)
@ExtendWith(MockitoExtension.class)
class EmpresaServiceTest {
@Mock
private EmpresaDAO empresaDAO;
@InjectMocks
private EmpresaService empresaService;
@Test
void devePersistirEmpresaComDadosValidos() {
// Arrange
Empresa empresa = new Empresa();
empresa.setRazaoSocial("TESTE LTDA");
empresa.setCnpj("12345678000199");
when(empresaDAO.existsByCnpj(anyString())).thenReturn(false);
when(empresaDAO.save(any(Empresa.class))).thenReturn(empresa);
// Act
Empresa resultado = empresaService.criar(empresa);
// Assert
assertNotNull(resultado);
verify(empresaDAO).save(empresa);
}
@Test
void deveRejeitarEmpresaComCnpjDuplicado() {
// Arrange
Empresa empresa = new Empresa();
empresa.setCnpj("12345678000199");
when(empresaDAO.existsByCnpj("12345678000199")).thenReturn(true);
// Act & Assert
assertThrows(CnpjDuplicadoException.class,
() -> empresaService.criar(empresa));
}
}
Testes de Integração (Arquillian/Docker)
@ExtendWith(ArquillianExtension.class)
class EmpresaIntegrationTest {
@Deployment
public static Archive<?> createDeployment() {
return ShrinkWrap.create(WebArchive.class)
.addPackages(true, "com.empresa.legado")
.addAsResource("persistence-test.xml", "META-INF/persistence.xml")
.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
}
@Inject
private EmpresaService empresaService;
@Test
@Transactional
void devePersistirNoBancoReal() {
// Arrange
Empresa empresa = new Empresa();
empresa.setRazaoSocial("INTEGRACAO TESTE LTDA");
empresa.setCnpj("98765432000199");
// Act
Empresa salva = empresaService.criar(empresa);
// Assert
assertNotNull(salva.getId());
// Verificar no banco
Empresa encontrada = empresaService.buscarPorId(salva.getId());
assertEquals("INTEGRACAO TESTE LTDA", encontrada.getRazaoSocial());
}
}
Testes E2E (Selenium - apenas fluxos críticos)
class TestFluxoCriticoEmpresa:
"""
Testes E2E - APENAS para fluxos críticos de negócio.
A lógica já foi validada nos testes de integração.
Aqui validamos a experiência do usuário.
"""
def test_fluxo_completo_cadastro_empresa(self):
"""
Valida fluxo completo: login -> cadastro -> verificação.
Este é UM teste que cobre o caminho feliz.
Cenários de erro são testados em integração.
"""
# Login
LoginPage(self.driver).fazer_login("admin", "admin123")
# Cadastro
ManterEmpresaPage(self.driver) \
.navegar() \
.clicar_nova_empresa() \
.preencher_formulario(
razao_social="E2E TESTE LTDA",
cnpj="11222333000144",
email="e2e@teste.com"
) \
.salvar()
# Verificação
empresas = ManterEmpresaPage(self.driver).obter_empresas_listadas()
assert any(e['razao_social'] == "E2E TESTE LTDA" for e in empresas)
CAPÍTULO 7: A EVOLUÇÃO — DE DOCUMENTAÇÃO PARA INFRAESTRUTURA
A maior lição aprendida nessa jornada foi uma mudança de perspectiva. No início, pensávamos em automação como documentação de falhas — uma forma de provar que bugs existiam.
Agora, pensamos em automação como infraestrutura de confiança — uma forma de garantir que o sistema funciona e continua funcionando.
A DIFERENÇA
| Documentação de Falhas | Infraestrutura de Confiança |
|---|---|
| Grave tudo, sempre | Grave apenas o necessário |
| Foco em mostrar o bug | Foco em prevenir o bug |
| Testes isolados por bug | Testes consolidados por funcionalidade |
| Vídeo como produto final | Vídeo como auxílio diagnóstico |
| Selenium para tudo | Pirâmide de testes completa |
| IDs instáveis | Data-attributes + Page Objects |
| Pixel = problema | Log = causa raiz |
COMO PREPARAR O JAVA EE PARA SER "AMIGÁVEL" À AUTOMAÇÃO
Se você está trabalhando com sistemas legados Java EE e quer facilitar a vida da automação:
-
Adicione data-test attributes no HTML
Coordene com o time de frontend para criar uma convenção de atributos testáveis.
-
Implemente Correlation ID
Use MDC do Logback/Log4j para rastrear requisições end-to-end.
-
Exponha endpoints de health check
Crie APIs que permitam aos testes verificar o estado do sistema sem depender da UI.
-
Padronize as respostas de erro
Garanta que erros retornem mensagens estruturadas, não páginas de erro genéricas.
-
Crie massa de dados via API
Testes não devem depender de dados pré-existentes. Exponha endpoints para setup/teardown de dados de teste.
-
Documente os estados da aplicação
Quais são os spinners de loading? Quais classes indicam sucesso/erro? Isso ajuda a criar esperas confiáveis.
EPÍLOGO: O LEGADO CONTINUA
O sistema legado ainda está lá. Ainda roda AngularJS 1.5. Ainda usa WildFly e Hibernate. Ainda tem stored procedures SQL Server. E ainda processa operações críticas todos os dias.
Mas agora, ele tem algo mais do que evidências visuais de problemas passados. Ele tem:
- Uma suíte de testes enxuta e rápida
- Page Objects — que absorvem mudanças de UI
- Correlação de logs — que acelera diagnósticos
- Captura condicional — que não afoga o CI/CD
- Uma pirâmide de testes — que valida cada camada
A automação deixou de ser nossa câmera de segurança — que apenas registra crimes depois que acontecem.
Ela se tornou nossa fundação — que previne que a estrutura desmorone.
E essa é a verdadeira evolução: de filmar problemas para impedir que eles existam. De arqueologia para arquitetura. De lentes que mostram o passado para alicerces que sustentam o futuro.
LIÇÕES FINAIS
-
Capture condicionalmente
Vídeo de teste que passou é espaço desperdiçado.
-
Page Object Model não é luxo
É sobrevivência em código legado.
-
Log conta mais que pixel
Implemente correlação de logs.
-
Absorva, não acumule
Testes de bug devem virar testes funcionais.
-
WebDriverWait sempre
pyautogui não tem lugar em testes web.
-
Pirâmide de testes
Selenium não substitui JUnit.
-
Prepare o legado
Data-attributes, APIs de health, logs estruturados.
A lente do Selenium é poderosa. Mas sozinha, ela apenas mostra.
Combinada com engenharia de testes madura, ela transforma.
E transformação é o que sistemas legados — e suas equipes — realmente precisam.
SOBRE ESTE ARTIGO
Este artigo é uma continuação de "A Arte de Iluminar o Passado" e representa a evolução de nossas práticas de automação de testes em sistemas legados JavaEE. As técnicas descritas foram refinadas através de meses de experiência prática.
Todos os nomes de empresas, aplicações e dados foram alterados para preservar a confidencialidade.
As práticas descritas são aplicáveis a qualquer sistema legado que busque evoluir de "documentação de falhas" para "infraestrutura de confiança".
PS.: GLOSSÁRIO DE TERMOS
Para facilitar a compreensão deste artigo por leitores de diferentes áreas, apresentamos um dicionário dos principais termos utilizados:
╔══════════════════════════════════════════════════════════════════════════════╗
║ TERMOS DE AUTOMAÇÃO E TESTES ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Selenium ─────────── Ferramenta que automatiza ações no navegador web, ║
║ simulando cliques, digitação e navegação. ║
║ ║
║ WebDriverWait ────── Mecanismo de espera que aguarda um elemento aparecer ║
║ antes de interagir, evitando erros por "pressa". ║
║ ║
║ Page Object Model ── Padrão onde cada tela tem sua "ficha técnica" com ║
║ (POM) botões e campos mapeados, facilitando manutenção. ║
║ ║
║ Data-Attributes ──── Etiquetas invisíveis (data-test) nos elementos HTML ║
║ para testes automatizados encontrá-los facilmente. ║
║ ║
║ Suíte de Regressão ─ Conjunto de testes que verificam se o sistema ║
║ continua funcionando após mudanças no código. ║
║ ║
║ Pirâmide de Testes ─ Estratégia: muitos unitários (base), alguns de ║
║ integração (meio), poucos E2E/Selenium (topo). ║
║ ║
║ Teste E2E ────────── Simula o caminho completo do usuário, do início ║
║ (End-to-End) ao fim de uma tarefa. ║
║ ║
║ Teste Unitário ───── Verifica uma única peça pequena do código ║
║ isoladamente (ex: apenas a função de soma). ║
║ ║
║ Teste de Integração─ Verifica se diferentes partes do sistema funcionam ║
║ bem juntas (frontend + backend + banco). ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ TERMOS DE INFRAESTRUTURA E DEVOPS ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ CI/CD ────────────── Processo automatizado que compila, testa e publica ║
║ software a cada mudança ("linha de montagem"). ║
║ ║
║ Pipeline ─────────── Sequência de etapas: compilar → testar → publicar. ║
║ ║
║ Jenkins ──────────── Ferramenta que executa pipelines CI/CD e gera ║
║ relatórios automaticamente. ║
║ ║
║ Artefatos ────────── Arquivos gerados: vídeos, relatórios, software ║
║ compilado. ║
║ ║
║ Correlation ID ───── Código único que rastreia uma requisição em todos ║
║ os logs do sistema, do início ao fim. ║
║ ║
║ MDC ──────────────── Recurso de logging que adiciona informações extras ║
║ (como Correlation ID) automaticamente nos logs. ║
║ ║
║ Logback / Log4j ──── Bibliotecas Java para geração de logs (registros ║
║ de eventos), essenciais para diagnóstico. ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ TERMOS DE TECNOLOGIA LEGADA ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Sistema Legado ───── Sistema antigo ainda em uso, crítico para negócio, ║
║ mas com tecnologias não mais preferidas. ║
║ ║
║ Java EE ──────────── Plataforma Java para aplicações empresariais ║
║ (Jakarta EE) robustas (web, banco de dados, integração). ║
║ ║
║ WildFly / JBoss ──── Servidor de aplicações Java que gerencia conexões, ║
║ transações e recursos empresariais. ║
║ ║
║ Hibernate / JPA ──── "Ponte" entre código Java e banco de dados, ║
║ traduzindo objetos em registros de tabelas. ║
║ ║
║ AngularJS 1.x ────── Framework JavaScript antigo para interfaces web. ║
║ Diferente do Angular moderno. Hoje é legado. ║
║ ║
║ Stored Procedure ─── Código SQL no banco que executa operações ║
║ complexas. Comum em sistemas legados. ║
║ ║
║ Stacktrace ───────── Rastro de erro que mostra onde o problema ocorreu, ║
║ listando a sequência de chamadas. ║
║ ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ TERMOS DE NEGÓCIO E PROCESSO ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ Stakeholder ──────── Qualquer pessoa interessada no projeto: gerentes, ║
║ usuários, clientes, desenvolvedores. ║
║ ║
║ Bug ──────────────── Erro ou defeito no software que causa comportamento ║
║ inesperado ou incorreto. ║
║ ║
║ Causa Raiz ───────── O motivo original de um problema, não apenas seus ║
║ sintomas visíveis. ║
║ ║
║ CRUD ─────────────── Create, Read, Update, Delete — as 4 operações ║
║ básicas de qualquer cadastro. ║
║ ║
║ Evidência ────────── Prova documentada de teste: screenshots, vídeos, ║
║ logs com resultado. ║
║ ║
║ Captura Condicional─ Técnica que só salva evidências quando o teste ║
║ falha, economizando armazenamento. ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════╝