💻 Exemplos: Capítulo 18: Arquitetura de Software em Camadas, Testes Automatizados com Pytest e Auditoria Contínua
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)