🧪 Guia de Testes - CSuite
Versão: 1.0
Última Atualização: 2025-01-02
Responsável: Equipe de Desenvolvimento
📋 Índice
- Visão Geral
- Tipos de Testes
- Estrutura de Testes
- Escrevendo Testes
- Executando Testes
- Cobertura de Testes
- Testes de Integração
- Testes E2E
- 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
- Testes Automatizados: Todos os testes devem ser automatizados
- Cobertura Adequada: Manter cobertura mínima de 70%
- Testes Rápidos: Testes unitários devem ser rápidos
- Testes Isolados: Testes não devem depender uns dos outros
- 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
- DEVELOPMENT_SETUP.md - Setup de desenvolvimento
- TROUBLESHOOTING.md - Troubleshooting
- RELEASE_PROCESS.md - Processo de release
Última Revisão: 2025-01-02
Próxima Revisão: 2025-04-02 (trimestral)