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

Além Da Lente - Os Limites da Automação Visual

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ões
  • data-test="input-*" para campos de entrada
  • data-test="link-*" para links
  • data-test="table-*" para tabelas
  • data-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:

  1. Validar a correção — com o teste de bug existente
  2. Identificar o teste funcional — que deveria cobrir esse cenário
  3. Adicionar o cenário ao teste funcional — ou criar se não existir
  4. Documentar no docstring — qual bug aquele teste previne
  5. Arquivar o teste de bug original — não deletar, mover para pasta de histórico
  6. 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:

  1. Adicione data-test attributes no HTML

    Coordene com o time de frontend para criar uma convenção de atributos testáveis.

  2. Implemente Correlation ID

    Use MDC do Logback/Log4j para rastrear requisições end-to-end.

  3. Exponha endpoints de health check

    Crie APIs que permitam aos testes verificar o estado do sistema sem depender da UI.

  4. Padronize as respostas de erro

    Garanta que erros retornem mensagens estruturadas, não páginas de erro genéricas.

  5. Crie massa de dados via API

    Testes não devem depender de dados pré-existentes. Exponha endpoints para setup/teardown de dados de teste.

  6. 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

  1. Capture condicionalmente

    Vídeo de teste que passou é espaço desperdiçado.

  2. Page Object Model não é luxo

    É sobrevivência em código legado.

  3. Log conta mais que pixel

    Implemente correlação de logs.

  4. Absorva, não acumule

    Testes de bug devem virar testes funcionais.

  5. WebDriverWait sempre

    pyautogui não tem lugar em testes web.

  6. Pirâmide de testes

    Selenium não substitui JUnit.

  7. 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.                     ║
║                                                                              ║
╚══════════════════════════════════════════════════════════════════════════════╝