Construindo um Framework de Testes Automatizados com Relatórios HTML Personalizados em Python

A criação de relatórios de testes visualmente atraentes e informativos é crucial para a comunicação eficiente dentro de equipes de desenvolvimento. Ao integrar o framework unittest, testes orientados a dados (DDT), manipulação de planilhas Excel, templates Jinja2 e geração HTML, é possível construir uma solução robusta que automatiza tanto a execução quanto a apresentação dos resultados.

Arquitetura do Fluxo de Trabalho O processo central envolve abstrair a lógica de teste para arquivos de dados externos, permitindo que analistas ou testadores definam casos sem escrever código Python. O framework utiliza o método discover() do unittest para localizar suítes de teste e executa-as gerando artefatos de saída. Em seguida, os resultados são processados por um template engine (Jinja2) para produzir documentos HTML dinâmicos. Por fim, esses relatórios podem ser distribuídos automaticamente via SMTP, Webhooks (DingTalk/WeCom) ou outras interfaces de notificação.

Implementação de Decors de Dados (Custom DDT) Para manter a compatibilidade com o padrão unittest enquanto se injeta dados dinamicamente, podemos criar decoradores personalizados que anexam metadados às funções de teste. Abaixo, apresentaoms uma implementação refatorada que utiliza YAML como fonte de dados alternativa e mantém a estrutura original de injeção de parâmetros:

import yaml

def inject_test_data(data_source):
    """
    Decorator to attach external data to a test function.
    This allows the test runner to iterate over datasets without modifying code logic.
    
    :param data_source: A list of dictionaries or a file path for YAML loading.
    :return: The decorated function with attached attributes.
    """
    def decorator(func):
        # If input is a string, assume it's a file path and load content
        if isinstance(data_source, str):
            try:
                with open(data_source, 'r', encoding='utf-8') as f:
                    loaded_data = yaml.safe_load(f)
            except UnicodeDecodeError:
                # Fallback encoding for legacy files
                with open(data_source, 'r', encoding='gbk') as f:
                    loaded_data = yaml.safe_load(f)
        else:
            loaded_data = data_source
            
        # Attach the dataset directly to the function object
        func.__dict__['TEST_DATASET'] = loaded_data
        return func
    return decorator

# Example usage structure for Excel-based injection would follow similar logic 
# but parse rows into lists of dicts before calling this decorator.

Estrutura Enxuta da Suíte de Testes Com a lógica de leitura de dados encapsulada em classes auxiliares (como Action ou DoExcel), a classe de teste principal torna-se minimalista. Ela delega a preparação, execução e validação para métodos utilitários, focando apenas no fluxo de controle.

import unittest
from config import Config
from utils.excel_handler import DoExcel
from utils.action_handler import Action

# Load initial configuration and test cases from Excel
excel_path = Config.TEST_CASE_PATH
excel_obj = DoExcel(excel_path)
test_cases, db_configs, init_data, api_host = excel_obj.extract_suite_info()

@inject_test_data(test_cases)
class ApiAutomationSuite(unittest.TestCase):
    maxDiff = None
    
    # Shared action instance for all tests in this class
    executor = Action(init_data, db_configs)

    @classmethod
    def setUpClass(cls):
        # Dynamically load helper modules or extensions if needed
        cls.executor.load_extensions(Config.EXTENSIONS_DIR)

    def setUp(self):
        pass

    def test_endpoint_validation(self, case_item):
        # Extract metadata from the current data row
        sheet_name, case_id, skip_flag, delay, name, desc, method, expected_result = self.executor.parse_case_metadata(case_item)
        
        # Check skip conditions defined in Excel
        if self.executor.should_skip(skip_flag):
            self.skipTest(f"Case {case_id} marked as skipped in configuration.")

        # Extract request parameters and dependencies
        regex_params, keys, deps, json_path_dict, request_payload = self.executor.get_request_details(case_item)
        
        # Apply artificial delay if specified
        self.executor.apply_delay(delay)
        
        # Execute pre-test SQL if required
        self.executor.run_sql_setup(case_item)
        
        # Skip API call if only SQL validation is intended
        if self.executor.is_sql_only(method):
            self.skipTest("SQL-only test case detected.")

        # Perform HTTP Request
        response = self.executor.send_http_request(api_host, method, request_payload)
        
        # Analyze response and extract values for subsequent steps
        self.executor.process_response(sheet_name, case_id, name, desc, regex_params, keys, deps, json_path_dict)
        
        # Validate against expected results
        self.executor.validate_results(excel_obj, sheet_name, case_id, name, desc, expected_result)

    @classmethod
    def tearDownClass(cls):
        excel_obj.close_file()

Abstração da Leitura de Planilhas A complexidade de mapear células do Excel para objetos Python é isolada em uma classe dedicada. Isso garante que mudanças na estrutura da planilha não quebrem a lógica de teste, desde que os nomes das colunas sejam preservados. A abordagem abaixo demonstra a conversão de linhas em dicionários usando um gerador (yield) para eficiência de memória.

class ExcelParser:
    def __init__(self, workbook):
        self.wb = workbook

    def get_sheet_names(self):
        # Assuming sheets are defined in a specific cell or config
        return ["Sheet1", "Sheet2"] 

    def iterate_rows_as_dicts(self, sheet_name):
        sheet = self.wb[sheet_name]
        max_row = sheet.max_row
        max_col = sheet.max_column
        
        headers = []
        # Read header row (assuming row 1)
        for col_idx in range(1, max_col + 1):
            headers.append(sheet.cell(row=1, column=col_idx).value)
            
        # Iterate through data rows starting from row 2
        for row_idx in range(2, max_row + 1):
            row_data = {}
            for col_idx, header in enumerate(headers, start=1):
                cell_value = sheet.cell(row=row_idx, column=col_idx).value
                row_data[header] = cell_value
            
            # Add sheet context to each record
            row_data['_sheet_name'] = sheet_name
            yield row_data

Geração de Relatórios HTML com Jinja2 Em vez de depender de bibliotecas de terceiros pesadas como Allure ou HTMLTestRunner nativo, a integração direta com Jinja2 oferece controle total sobre a estética e a estrutura do relatório. O resultado final substitui logs textuais brutos por dashboards visuais que detalham entradas, saídas esperadas e status reais. Essa customização permite incluir gráficos, cores condicionais para falhas/sucessos e links diretos para logs específicos, melhorando significativamente a legibilidade para stakeholders não técnicos.

Distribuição Automática de Resultados A utilidade do framework aumenta quando os relatórios são entregues proativamente. Após a conclusão da suíte de testes, o caminho do arquivo HTML gerado pode ser passado para módulos de notificação. Integrantes comuns incluem:

  • SMTP: Para envio de e-mails corporativos com anexos.
  • Webhooks (DingTalk/WeCom/Slack): Para mensagens instantâneas em canais de equipe, muitas vezes suportando markdown ou cards interativos. Essa automação elimina a necessidade de configuração manual de servidores CI/CD para visualização básica de testes, reduzindo a sobrecarga operacional associada à manutenção de plug-ins complexos.

Tags: Python unittest jinja2 Data-Driven Testing Excel Automation

Publicado em 10-8 21:34