Testing Guide

🧪 Guia de Testes - CSuite

Versão: 1.0
Última Atualização: 2025-01-02
Responsável: Equipe de Desenvolvimento


📋 Índice

  1. Visão Geral
  2. Tipos de Testes
  3. Estrutura de Testes
  4. Escrevendo Testes
  5. Executando Testes
  6. Cobertura de Testes
  7. Testes de Integração
  8. Testes E2E
  9. CI/CD

🎯 Visão Geral

Este guia descreve a estratégia e práticas de testes para o ecossistema CSuite, garantindo qualidade e confiabilidade do código.

Princípios

  1. Testes Automatizados: Todos os testes devem ser automatizados
  2. Cobertura Adequada: Manter cobertura mínima de 70%
  3. Testes Rápidos: Testes unitários devem ser rápidos
  4. Testes Isolados: Testes não devem depender uns dos outros
  5. Testes Determinísticos: Testes devem ter resultados consistentes

📦 Tipos de Testes

Testes Unitários

O que testar:
- Funções individuais
- Métodos de classes
- Lógica de negócio
- Validações

Características:
- Rápidos (< 1 segundo cada)
- Isolados (sem dependências externas)
- Determinísticos (mesmo resultado sempre)

Exemplo:

def test_calculate_total():
    assert calculate_total([10, 20, 30]) == 60
    assert calculate_total([]) == 0

Testes de Integração

O que testar:
- Integração entre componentes
- Integração com banco de dados
- Integração com APIs externas
- Fluxos completos

Características:
- Mais lentos que unitários
- Podem ter dependências externas
- Testam interações reais

Exemplo:

def test_create_decision_integration():
    # Testa criação de decisão com banco real
    decision = create_decision(context_id=123)
    assert decision.id is not None
    assert decision.status == "pending"

Testes E2E (End-to-End)

O que testar:
- Fluxos completos do usuário
- Integração entre serviços
- Comportamento do sistema completo

Características:
- Mais lentos
- Requerem ambiente completo
- Testam cenários reais

Exemplo:

def test_decision_flow_e2e():
    # Testa fluxo completo: contexto -> decisão -> outcome
    context = create_context()
    decision = trigger_decision(context.id)
    outcome = wait_for_outcome(decision.id)
    assert outcome.status == "completed"

📁 Estrutura de Testes

Organização

c-suite/
├── tests/
│   ├── unit/
│   │   ├── test_utils.py
│   │   ├── test_validators.py
│   │   └── test_models.py
│   ├── integration/
│   │   ├── test_api.py
│   │   ├── test_database.py
│   │   └── test_services.py
│   ├── e2e/
│   │   ├── test_decision_flow.py
│   │   └── test_agent_loop.py
│   ├── fixtures/
│   │   ├── sample_data.py
│   │   └── mocks.py
│   └── conftest.py

conftest.py

Fixtures compartilhadas:

# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

@pytest.fixture
def client():
    return TestClient(app)

@pytest.fixture
def db_session():
    # Setup database session
    session = create_session()
    yield session
    # Teardown
    session.close()

✍️ Escrevendo Testes

Padrão AAA (Arrange, Act, Assert)

Estrutura:

def test_example():
    # Arrange: Preparar dados
    data = {"name": "Test", "value": 123}

    # Act: Executar ação
    result = process_data(data)

    # Assert: Verificar resultado
    assert result.status == "success"
    assert result.value == 123

Testes de API

Exemplo:

def test_get_health(client):
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

def test_create_decision(client, db_session):
    payload = {
        "context_id": 123,
        "entity_id": 456
    }
    response = client.post("/api/v1/decisions", json=payload)
    assert response.status_code == 201
    assert response.json()["id"] is not None

Testes de Banco de Dados

Exemplo:

def test_create_context(db_session):
    context = Context(
        entity_id=123,
        entity_type="revenda",
        data={"key": "value"}
    )
    db_session.add(context)
    db_session.commit()

    result = db_session.query(Context).filter_by(entity_id=123).first()
    assert result is not None
    assert result.data == {"key": "value"}

Mocks e Stubs

Exemplo:

from unittest.mock import Mock, patch

@patch('app.external_api.call')
def test_with_mock(mock_call):
    mock_call.return_value = {"status": "ok"}

    result = my_function()

    assert result == {"status": "ok"}
    mock_call.assert_called_once()

🏃 Executando Testes

Comandos Básicos

Todos os testes:

pytest

Testes específicos:

# Por arquivo
pytest tests/test_api.py

# Por função
pytest tests/test_api.py::test_get_health

# Por padrão
pytest tests/unit/

Com opções:

# Verbose
pytest -v

# Mostrar prints
pytest -s

# Parar no primeiro erro
pytest -x

# Executar apenas testes que falharam
pytest --lf

Markers

Marcar testes:

import pytest

@pytest.mark.unit
def test_unit():
    pass

@pytest.mark.integration
def test_integration():
    pass

@pytest.mark.slow
def test_slow():
    pass

Executar por marker:

# Apenas unitários
pytest -m unit

# Apenas integração
pytest -m integration

# Excluir lentos
pytest -m "not slow"

📊 Cobertura de Testes

Medir Cobertura

Instalar:

pip install pytest-cov

Executar com cobertura:

# Cobertura básica
pytest --cov=app

# Cobertura com relatório HTML
pytest --cov=app --cov-report=html

# Cobertura com relatório terminal
pytest --cov=app --cov-report=term-missing

Verificar cobertura mínima:

pytest --cov=app --cov-fail-under=70

Configuração

pytest.ini:

[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = 
    --cov=app
    --cov-report=html
    --cov-report=term-missing
    --cov-fail-under=70

🔗 Testes de Integração

Setup de Ambiente

docker-compose.test.yml:

version: '3.8'
services:
  mysql-test:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: test_password
      MYSQL_DATABASE: csuite_test
    ports:
      - "3307:3306"

Executar:

docker-compose -f docker-compose.test.yml up -d
pytest tests/integration/
docker-compose -f docker-compose.test.yml down

Testes de API com Banco Real

Exemplo:

@pytest.fixture
def test_db():
    # Setup test database
    engine = create_engine("mysql://root:test@localhost:3307/csuite_test")
    Base.metadata.create_all(engine)
    yield engine
    # Teardown
    Base.metadata.drop_all(engine)

def test_api_with_db(client, test_db):
    # Test API with real database
    response = client.post("/api/v1/contexts", json={"entity_id": 123})
    assert response.status_code == 201

🌐 Testes E2E

Setup Completo

Executar todos os serviços:

docker-compose up -d
pytest tests/e2e/
docker-compose down

Exemplo de Teste E2E

def test_decision_flow_e2e():
    # 1. Criar contexto
    context_response = requests.post(
        "http://localhost:8080/api/v1/contexts",
        json={"entity_id": 123, "entity_type": "revenda"}
    )
    context_id = context_response.json()["id"]

    # 2. Trigger decisão
    decision_response = requests.post(
        f"http://localhost:8080/api/v1/decisions",
        json={"context_id": context_id}
    )
    decision_id = decision_response.json()["id"]

    # 3. Aguardar outcome
    outcome = wait_for_outcome(decision_id, timeout=30)
    assert outcome["status"] == "completed"

🔄 CI/CD

GitHub Actions

.github/workflows/tests.yml:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest pytest-cov
      - name: Run tests
        run: pytest --cov=app --cov-report=xml
      - name: Upload coverage
        uses: codecov/codecov-action@v2

📚 Boas Práticas

Nomenclatura

Nomes descritivos:

# Bom
def test_create_decision_with_valid_context():
    pass

# Ruim
def test1():
    pass

Um Assert por Teste (quando possível)

Bom:

def test_decision_status():
    decision = create_decision()
    assert decision.status == "pending"

Aceitável (múltiplos asserts relacionados):

def test_decision_creation():
    decision = create_decision()
    assert decision.id is not None
    assert decision.status == "pending"
    assert decision.created_at is not None

Testes Independentes

Bom:

def test_create_decision():
    decision = create_decision()
    assert decision.id is not None

def test_get_decision():
    decision = create_decision()
    result = get_decision(decision.id)
    assert result.id == decision.id

Evitar:

# Testes dependentes (evitar)
decision_id = None

def test_create():
    global decision_id
    decision = create_decision()
    decision_id = decision.id

def test_get():
    # Depende de test_create ter executado antes
    result = get_decision(decision_id)

🔗 Documentos Relacionados


Última Revisão: 2025-01-02
Próxima Revisão: 2025-04-02 (trimestral)

🔊 Text-to-Speech

1.0x
1.0
Pronto para reproduzir