Especialização em Sistemas com Python 3.12+ • Capítulo 18


🎯 Demonstração Prática: Bateria de Testes Automatizados e Auditoria Contínua com Pytest

O exemplo abaixo demonstra a estrutura de testes automatizados e geração de trilhas de auditoria contínua:

  • Módulo de regras de negócio financeiras desacoplado em camadas.
  • Suíte de testes com asserções declarativas, validações de limites (Edge Cases) e tratamento de exceções com pytest.raises.
  • Gerador corporativo de trilha de auditoria contínua serializada em arquivo JSON datado no formato reports/YYYY-MM-DD_HH-MM-SS_auditoria.json.
"""
demonstracao_testes_e_auditoria.py
Arquitetura em Camadas, Testes com Pytest e Trilha de Auditoria Contínua.
Compatível com Python 3.12+ (Executável standalone ou via pytest).
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_EVEN
import json
from pathlib import Path
import sys
from typing import Final

# ==============================================================================
# 1. CAMADA DE DOMÍNIO E REGRAS DE NEGÓCIO
# ==============================================================================
class SaldoInsuficienteError(Exception):
    pass

@dataclass(slots=True)
class ContaCorrente:
    id_conta: str
    titular: str
    saldo: Decimal
    limite_especial: Decimal = Decimal("0.00")

    def sacar(self, valor: Decimal) -> Decimal:
        if valor <= Decimal("0.00"):
            raise ValueError(f"Valor de saque deve ser positivo: {valor}")
        
        disponivel_total = self.saldo + self.limite_especial
        if valor > disponivel_total:
            raise SaldoInsuficienteError(
                f"Fundos insuficientes. Solicitado: R$ {valor:,.2f} | Disponível: R$ {disponivel_total:,.2f}"
            )

        self.saldo = (self.saldo - valor).quantize(Decimal("0.01"), ROUND_HALF_EVEN)
        return self.saldo

    def depositar(self, valor: Decimal) -> Decimal:
        if valor <= Decimal("0.00"):
            raise ValueError(f"Valor de depósito deve ser positivo: {valor}")
        self.saldo = (self.saldo + valor).quantize(Decimal("0.01"), ROUND_HALF_EVEN)
        return self.saldo

# ==============================================================================
# 2. MOTOR DE AUDITORIA CONTÍNUA
# ==============================================================================
class AuditoriaService:
    @staticmethod
    def gerar_trilha_auditoria(
        diretorio_saida: Path,
        operador: str,
        operacao: str,
        detalhes: dict[str, object]
    ) -> Path:
        """Gera arquivo JSON padronizado com carimbo de tempo UTC e metadados."""
        diretorio_saida.mkdir(parents=True, exist_ok=True)
        agora = datetime.now(timezone.utc)
        timestamp_slug = agora.strftime("%Y-%m-%d_%H-%M-%S")
        arquivo_auditoria = diretorio_saida / f"{timestamp_slug}_auditoria.json"

        payload = {
            "timestamp_utc": agora.isoformat(),
            "operador_executor": operador,
            "tipo_operacao": operacao,
            "status": "CONFORME",
            "detalhes": detalhes
        }

        arquivo_auditoria.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
        return arquivo_auditoria

# ==============================================================================
# 3. BATERIA DE TESTES AUTOMATIZADOS (COMPATÍVEL COM PYTEST)
# ==============================================================================
def test_deposito_bem_sucedido() -> None:
    conta = ContaCorrente("CC-001", "Mariana", Decimal("500.00"))
    novo_saldo = conta.depositar(Decimal("250.00"))
    assert novo_saldo == Decimal("750.00")
    assert conta.saldo == Decimal("750.00")

def test_saque_dentro_do_limite_especial() -> None:
    conta = ContaCorrente("CC-002", "Carlos", Decimal("100.00"), limite_especial=Decimal("400.00"))
    novo_saldo = conta.sacar(Decimal("300.00")) # Entra em R$ 200,00 de limite
    assert novo_saldo == Decimal("-200.00")

def test_saque_superior_ao_limite_dispara_excecao() -> None:
    conta = ContaCorrente("CC-003", "Beatriz", Decimal("200.00"), limite_especial=Decimal("100.00"))
    try:
        conta.sacar(Decimal("500.00"))
        assert False, "Deveria ter lançado SaldoInsuficienteError"
    except SaldoInsuficienteError as err:
        assert "Fundos insuficientes" in str(err)

def test_deposito_negativo_rejeitado() -> None:
    conta = ContaCorrente("CC-004", "Lucas", Decimal("1000.00"))
    try:
        conta.depositar(Decimal("-50.00"))
        assert False, "Deveria ter lançado ValueError"
    except ValueError:
        pass # Comportamento esperado

# ==============================================================================
# 4. EXECUÇÃO STANDALONE
# ==============================================================================
if __name__ == "__main__":
    print("=" * 80)
    print("      BATERIA DE TESTES AUTOMATIZADOS E AUDITORIA CONTÍNUA (PYTEST)")
    print("=" * 80)

    # Executa a bateria de testes manualmente
    testes = [
        ("Depósito Válido", test_deposito_bem_sucedido),
        ("Saque com Limite Especial", test_saque_dentro_do_limite_especial),
        ("Estouro de Saldo (Exceção)", test_saque_superior_ao_limite_dispara_excecao),
        ("Depósito Negativo (Validação)", test_deposito_negativo_rejeitado),
    ]

    for nome_teste, fn_teste in testes:
        try:
            fn_teste()
            print(f"  [PASS] {nome_teste:<35} -> OK")
        except AssertionError as err:
            print(f"  [FAIL] {nome_teste:<35} -> FALHOU: {err}")

    print("-" * 80)
    print(">> Gerando Trilha de Auditoria Corporativa (reports/)...")
    pasta_reports = Path("reports_temp_demo")

    arquivo_gerado = AuditoriaService.gerar_trilha_auditoria(
        diretorio_saida=pasta_reports,
        operador="ci_cd_runner_automation",
        operacao="SUITE_TESTES_REGRESSAO",
        detalhes={
            "total_testes_executados": len(testes),
            "status_geral": "100%_PASSOU",
            "ambiente": "Staging / Python 3.12+"
        }
    )

    print(f"Relatório Datado Gerado em: {arquivo_gerado}")
    print("\nConteúdo do Arquivo de Auditoria:")
    print(arquivo_gerado.read_text(encoding="utf-8"))

    # Limpeza do mock de teste
    arquivo_gerado.unlink()
    pasta_reports.rmdir()
    print("-" * 80)
    print(">> Ambiente de teste limpo com sucesso.")
    print("=" * 80)