Sprint 4 deliverables: - DATABASE-SCHEMA.md: ER diagrams for 12 schemas (~90 tables) - TESTING-STRATEGY.md: Testing pyramid (70/20/10), frameworks, CI/CD Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
639 lines
14 KiB
Markdown
639 lines
14 KiB
Markdown
# TESTING-STRATEGY.md - Trading Platform
|
|
|
|
**Version:** 1.0.0
|
|
**Fecha:** 2026-01-30
|
|
**Proyecto:** trading-platform
|
|
|
|
---
|
|
|
|
## Resumen Ejecutivo
|
|
|
|
Este documento define la estrategia de testing para Trading Platform, cubriendo todos los niveles de la pirámide de testing y consideraciones específicas para sistemas de trading con ML.
|
|
|
|
---
|
|
|
|
## Niveles de Testing
|
|
|
|
### Pirámide de Testing
|
|
|
|
```
|
|
┌─────────────┐
|
|
│ E2E │ 10%
|
|
│ Tests │
|
|
┌──┴─────────────┴──┐
|
|
│ Integration │ 20%
|
|
│ Tests │
|
|
┌──┴───────────────────┴──┐
|
|
│ Unit Tests │ 70%
|
|
└─────────────────────────┘
|
|
```
|
|
|
|
| Nivel | Porcentaje | Responsabilidad |
|
|
|-------|------------|-----------------|
|
|
| Unit | 70% | Lógica de negocio aislada |
|
|
| Integration | 20% | Interacción entre módulos |
|
|
| E2E | 10% | Flujos críticos completos |
|
|
|
|
---
|
|
|
|
## 1. Unit Tests
|
|
|
|
### 1.1 Backend (TypeScript/Express)
|
|
|
|
**Framework:** Jest + ts-jest
|
|
|
|
**Estructura:**
|
|
```
|
|
apps/backend/src/
|
|
├── modules/
|
|
│ ├── auth/
|
|
│ │ ├── services/
|
|
│ │ │ ├── token.service.ts
|
|
│ │ │ └── __tests__/
|
|
│ │ │ └── token.service.spec.ts
|
|
│ │ └── ...
|
|
│ └── ...
|
|
```
|
|
|
|
**Convenciones:**
|
|
- Archivos: `*.spec.ts` junto al archivo fuente
|
|
- Naming: `describe('ServiceName')` > `describe('methodName')` > `it('should...')`
|
|
- Mocks: Usar `jest.mock()` para dependencias externas
|
|
|
|
**Ejemplo:**
|
|
```typescript
|
|
// token.service.spec.ts
|
|
describe('TokenService', () => {
|
|
describe('generateAccessToken', () => {
|
|
it('should return a valid JWT token', () => {
|
|
const token = tokenService.generateAccessToken(mockUser);
|
|
expect(token).toMatch(/^eyJ/);
|
|
});
|
|
|
|
it('should include user id in payload', () => {
|
|
const token = tokenService.generateAccessToken(mockUser);
|
|
const decoded = jwt.decode(token);
|
|
expect(decoded.sub).toBe(mockUser.id);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
**Coverage mínimo:** 80%
|
|
|
|
### 1.2 Frontend (React)
|
|
|
|
**Framework:** Vitest + React Testing Library
|
|
|
|
**Estructura:**
|
|
```
|
|
apps/frontend/src/
|
|
├── components/
|
|
│ ├── Button/
|
|
│ │ ├── Button.tsx
|
|
│ │ └── Button.test.tsx
|
|
│ └── ...
|
|
├── hooks/
|
|
│ ├── useAuth.ts
|
|
│ └── __tests__/
|
|
│ └── useAuth.test.ts
|
|
```
|
|
|
|
**Convenciones:**
|
|
- Archivos: `*.test.tsx` o `*.test.ts`
|
|
- Testing Library: Preferir queries por rol y accesibilidad
|
|
- Avoid testing implementation details
|
|
|
|
**Ejemplo:**
|
|
```typescript
|
|
// Button.test.tsx
|
|
describe('Button', () => {
|
|
it('should call onClick when clicked', async () => {
|
|
const handleClick = vi.fn();
|
|
render(<Button onClick={handleClick}>Click me</Button>);
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: /click me/i }));
|
|
|
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
```
|
|
|
|
**Coverage mínimo:** 70%
|
|
|
|
### 1.3 ML Engine (Python)
|
|
|
|
**Framework:** pytest + pytest-cov
|
|
|
|
**Estructura:**
|
|
```
|
|
apps/ml-engine/
|
|
├── src/
|
|
│ ├── models/
|
|
│ │ └── range_predictor.py
|
|
│ └── ...
|
|
├── tests/
|
|
│ ├── unit/
|
|
│ │ ├── test_range_predictor.py
|
|
│ │ └── test_feature_engineering.py
|
|
│ └── ...
|
|
```
|
|
|
|
**Convenciones:**
|
|
- Archivos: `test_*.py`
|
|
- Fixtures: Usar `@pytest.fixture` para datos de prueba
|
|
- Parametrize: Usar `@pytest.mark.parametrize` para múltiples casos
|
|
|
|
**Ejemplo:**
|
|
```python
|
|
# test_range_predictor.py
|
|
class TestRangePredictor:
|
|
@pytest.fixture
|
|
def predictor(self):
|
|
return RangePredictor(symbol='XAUUSD')
|
|
|
|
def test_predict_returns_valid_range(self, predictor, sample_features):
|
|
result = predictor.predict(sample_features)
|
|
|
|
assert 'predicted_high' in result
|
|
assert 'predicted_low' in result
|
|
assert result['predicted_high'] > result['predicted_low']
|
|
|
|
@pytest.mark.parametrize('confidence_threshold', [0.5, 0.7, 0.9])
|
|
def test_filter_by_confidence(self, predictor, confidence_threshold):
|
|
# Test filtering logic
|
|
pass
|
|
```
|
|
|
|
**Coverage mínimo:** 75%
|
|
|
|
---
|
|
|
|
## 2. Integration Tests
|
|
|
|
### 2.1 API Integration Tests
|
|
|
|
**Framework:** Jest + Supertest
|
|
|
|
**Objetivo:** Verificar endpoints con base de datos real (test DB)
|
|
|
|
**Setup:**
|
|
```typescript
|
|
// setup/test-db.ts
|
|
beforeAll(async () => {
|
|
await setupTestDatabase();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await teardownTestDatabase();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await clearTables();
|
|
});
|
|
```
|
|
|
|
**Ejemplo:**
|
|
```typescript
|
|
// auth.integration.spec.ts
|
|
describe('Auth API Integration', () => {
|
|
describe('POST /api/v1/auth/login', () => {
|
|
it('should return tokens for valid credentials', async () => {
|
|
// Arrange
|
|
await createTestUser({ email: 'test@example.com', password: 'Test123!' });
|
|
|
|
// Act
|
|
const response = await request(app)
|
|
.post('/api/v1/auth/login')
|
|
.send({ email: 'test@example.com', password: 'Test123!' });
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toHaveProperty('accessToken');
|
|
expect(response.body.data).toHaveProperty('refreshToken');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 2.2 Database Integration Tests
|
|
|
|
**Objetivo:** Verificar queries, triggers y funciones PL/pgSQL
|
|
|
|
```typescript
|
|
// wallet.integration.spec.ts
|
|
describe('Wallet Transactions', () => {
|
|
it('should update balance after deposit', async () => {
|
|
const wallet = await createTestWallet({ balance: 100 });
|
|
|
|
await db.query(
|
|
'SELECT financial.process_transaction($1, $2, $3)',
|
|
[wallet.id, 'deposit', 50]
|
|
);
|
|
|
|
const updated = await getWallet(wallet.id);
|
|
expect(updated.balance).toBe(150);
|
|
});
|
|
});
|
|
```
|
|
|
|
### 2.3 ML Pipeline Integration Tests
|
|
|
|
**Objetivo:** Verificar pipeline completo de predicción
|
|
|
|
```python
|
|
# test_prediction_pipeline.py
|
|
class TestPredictionPipeline:
|
|
def test_full_prediction_flow(self, db_session, redis_client):
|
|
# 1. Load features from feature store
|
|
features = load_features('XAUUSD', '2024-01-01')
|
|
|
|
# 2. Run prediction
|
|
prediction = run_prediction(features)
|
|
|
|
# 3. Verify stored in database
|
|
stored = db_session.query(Prediction).filter_by(
|
|
symbol='XAUUSD'
|
|
).first()
|
|
|
|
assert stored is not None
|
|
assert stored.predicted_high == prediction['high']
|
|
```
|
|
|
|
---
|
|
|
|
## 3. E2E Tests
|
|
|
|
### 3.1 Framework
|
|
|
|
**Herramienta:** Playwright
|
|
|
|
**Estructura:**
|
|
```
|
|
e2e/
|
|
├── tests/
|
|
│ ├── auth/
|
|
│ │ ├── login.spec.ts
|
|
│ │ └── register.spec.ts
|
|
│ ├── trading/
|
|
│ │ └── place-order.spec.ts
|
|
│ └── ...
|
|
├── fixtures/
|
|
│ └── test-data.ts
|
|
└── playwright.config.ts
|
|
```
|
|
|
|
### 3.2 Flujos Críticos a Testear
|
|
|
|
| Prioridad | Flujo | Descripción |
|
|
|-----------|-------|-------------|
|
|
| P0 | Login/Logout | Autenticación completa |
|
|
| P0 | Registro + Verificación | Nuevo usuario |
|
|
| P0 | Depositar fondos | Wallet + Stripe |
|
|
| P1 | Abrir cuenta inversión | PAMM flow |
|
|
| P1 | Paper trading | Orden → Posición |
|
|
| P2 | Completar curso | Education flow |
|
|
| P2 | 2FA setup | Security flow |
|
|
|
|
### 3.3 Ejemplo E2E
|
|
|
|
```typescript
|
|
// e2e/tests/auth/login.spec.ts
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('Login Flow', () => {
|
|
test('user can login with valid credentials', async ({ page }) => {
|
|
// Navigate
|
|
await page.goto('/login');
|
|
|
|
// Fill form
|
|
await page.getByLabel('Email').fill('test@example.com');
|
|
await page.getByLabel('Password').fill('Test123!');
|
|
|
|
// Submit
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
// Verify redirect to dashboard
|
|
await expect(page).toHaveURL('/dashboard');
|
|
await expect(page.getByText('Welcome back')).toBeVisible();
|
|
});
|
|
|
|
test('shows error for invalid credentials', async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.getByLabel('Email').fill('wrong@example.com');
|
|
await page.getByLabel('Password').fill('wrongpassword');
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
await expect(page.getByText(/invalid credentials/i)).toBeVisible();
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Testing Específico para Trading
|
|
|
|
### 4.1 Backtesting de Modelos ML
|
|
|
|
**Objetivo:** Validar performance de modelos con datos históricos
|
|
|
|
```python
|
|
# tests/backtesting/test_model_performance.py
|
|
class TestModelBacktest:
|
|
def test_model_accuracy_oos(self):
|
|
"""Test Out-of-Sample accuracy"""
|
|
# Load 12 months of excluded data
|
|
oos_data = load_oos_data('2025-01', '2025-12')
|
|
|
|
# Run predictions
|
|
predictions = model.predict(oos_data.features)
|
|
|
|
# Calculate metrics
|
|
accuracy = calculate_directional_accuracy(
|
|
predictions,
|
|
oos_data.actual
|
|
)
|
|
|
|
# Minimum threshold
|
|
assert accuracy >= 0.55, f"OOS accuracy {accuracy} below threshold"
|
|
```
|
|
|
|
### 4.2 Paper Trading Tests
|
|
|
|
**Objetivo:** Simular operaciones sin riesgo
|
|
|
|
```typescript
|
|
// tests/paper-trading/order-execution.spec.ts
|
|
describe('Paper Trading Order Execution', () => {
|
|
it('should execute market order at current price', async () => {
|
|
const account = await createPaperAccount({ balance: 10000 });
|
|
|
|
const order = await paperTradingService.placeOrder({
|
|
accountId: account.id,
|
|
symbol: 'XAUUSD',
|
|
side: 'BUY',
|
|
type: 'MARKET',
|
|
quantity: 0.1
|
|
});
|
|
|
|
expect(order.status).toBe('FILLED');
|
|
expect(order.filledPrice).toBeCloseTo(currentPrice, 2);
|
|
});
|
|
});
|
|
```
|
|
|
|
### 4.3 Risk Management Tests
|
|
|
|
```typescript
|
|
// tests/risk/position-limits.spec.ts
|
|
describe('Position Limits', () => {
|
|
it('should reject order exceeding max position size', async () => {
|
|
const result = await riskService.validateOrder({
|
|
userId: testUser.id,
|
|
symbol: 'XAUUSD',
|
|
quantity: 100 // Exceeds limit
|
|
});
|
|
|
|
expect(result.allowed).toBe(false);
|
|
expect(result.reason).toContain('position limit');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Mocking Strategies
|
|
|
|
### 5.1 External Services
|
|
|
|
| Servicio | Estrategia |
|
|
|----------|------------|
|
|
| Stripe | Mock SDK responses |
|
|
| Binance API | Recorded fixtures |
|
|
| Redis | Redis-mock o real |
|
|
| LLM (Claude) | Canned responses |
|
|
|
|
### 5.2 Mock de Stripe
|
|
|
|
```typescript
|
|
// __mocks__/stripe.ts
|
|
export const mockStripe = {
|
|
customers: {
|
|
create: jest.fn().mockResolvedValue({ id: 'cus_test123' }),
|
|
retrieve: jest.fn().mockResolvedValue({ id: 'cus_test123', email: 'test@example.com' })
|
|
},
|
|
subscriptions: {
|
|
create: jest.fn().mockResolvedValue({ id: 'sub_test123', status: 'active' })
|
|
}
|
|
};
|
|
|
|
jest.mock('stripe', () => ({
|
|
__esModule: true,
|
|
default: jest.fn(() => mockStripe)
|
|
}));
|
|
```
|
|
|
|
### 5.3 Mock de Market Data
|
|
|
|
```python
|
|
# tests/fixtures/market_data.py
|
|
@pytest.fixture
|
|
def mock_binance_client(mocker):
|
|
mock = mocker.patch('binance.Client')
|
|
mock.return_value.get_klines.return_value = [
|
|
[1704067200000, "2050.00", "2055.00", "2048.00", "2052.00", "1000"],
|
|
[1704067260000, "2052.00", "2058.00", "2051.00", "2056.00", "1200"],
|
|
]
|
|
return mock
|
|
```
|
|
|
|
---
|
|
|
|
## 6. CI/CD Integration
|
|
|
|
### 6.1 GitHub Actions Workflow
|
|
|
|
```yaml
|
|
# .github/workflows/test.yml
|
|
name: Tests
|
|
|
|
on: [push, pull_request]
|
|
|
|
jobs:
|
|
unit-tests:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Setup Node
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
- name: Run unit tests
|
|
run: npm run test:unit
|
|
- name: Upload coverage
|
|
uses: codecov/codecov-action@v3
|
|
|
|
integration-tests:
|
|
runs-on: ubuntu-latest
|
|
services:
|
|
postgres:
|
|
image: postgres:16
|
|
env:
|
|
POSTGRES_DB: trading_test
|
|
POSTGRES_USER: test
|
|
POSTGRES_PASSWORD: test
|
|
ports:
|
|
- 5432:5432
|
|
redis:
|
|
image: redis:7
|
|
ports:
|
|
- 6379:6379
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Run integration tests
|
|
run: npm run test:integration
|
|
|
|
e2e-tests:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Install Playwright
|
|
run: npx playwright install --with-deps
|
|
- name: Run E2E tests
|
|
run: npm run test:e2e
|
|
```
|
|
|
|
### 6.2 Pre-commit Hooks
|
|
|
|
```yaml
|
|
# .pre-commit-config.yaml
|
|
repos:
|
|
- repo: local
|
|
hooks:
|
|
- id: test-affected
|
|
name: Run affected tests
|
|
entry: npm run test:affected
|
|
language: system
|
|
pass_filenames: false
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Test Data Management
|
|
|
|
### 7.1 Factories
|
|
|
|
```typescript
|
|
// tests/factories/user.factory.ts
|
|
export const userFactory = {
|
|
build: (overrides = {}) => ({
|
|
id: faker.string.uuid(),
|
|
email: faker.internet.email(),
|
|
passwordHash: bcrypt.hashSync('Test123!', 10),
|
|
status: 'active',
|
|
role: 'user',
|
|
...overrides
|
|
}),
|
|
|
|
create: async (overrides = {}) => {
|
|
const user = userFactory.build(overrides);
|
|
await db.query('INSERT INTO auth.users ...', [user]);
|
|
return user;
|
|
}
|
|
};
|
|
```
|
|
|
|
### 7.2 Fixtures para ML
|
|
|
|
```python
|
|
# tests/conftest.py
|
|
@pytest.fixture(scope='session')
|
|
def sample_ohlcv_data():
|
|
return pd.read_parquet('tests/fixtures/xauusd_sample.parquet')
|
|
|
|
@pytest.fixture(scope='session')
|
|
def trained_model():
|
|
return joblib.load('tests/fixtures/model_v1.joblib')
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Comandos de Ejecución
|
|
|
|
### Backend
|
|
```bash
|
|
# Unit tests
|
|
npm run test:unit
|
|
|
|
# Integration tests
|
|
npm run test:integration
|
|
|
|
# Coverage
|
|
npm run test:coverage
|
|
```
|
|
|
|
### Frontend
|
|
```bash
|
|
# Unit tests
|
|
npm run test
|
|
|
|
# Watch mode
|
|
npm run test:watch
|
|
|
|
# Coverage
|
|
npm run test:coverage
|
|
```
|
|
|
|
### ML Engine
|
|
```bash
|
|
# All tests
|
|
pytest
|
|
|
|
# Unit only
|
|
pytest tests/unit/
|
|
|
|
# With coverage
|
|
pytest --cov=src --cov-report=html
|
|
```
|
|
|
|
### E2E
|
|
```bash
|
|
# Headless
|
|
npm run test:e2e
|
|
|
|
# UI mode
|
|
npm run test:e2e:ui
|
|
|
|
# Specific browser
|
|
npx playwright test --project=chromium
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Métricas y Umbrales
|
|
|
|
| Métrica | Umbral | Medición |
|
|
|---------|--------|----------|
|
|
| Unit Test Coverage | ≥ 70% | Codecov |
|
|
| Integration Test Coverage | ≥ 50% | Codecov |
|
|
| E2E Critical Paths | 100% pass | Playwright |
|
|
| ML Model Accuracy (OOS) | ≥ 55% | Custom |
|
|
| Test Execution Time | < 10 min | CI |
|
|
|
|
---
|
|
|
|
## 10. Próximos Pasos
|
|
|
|
1. **Implementar tests faltantes por módulo**
|
|
2. **Configurar CI/CD con GitHub Actions**
|
|
3. **Integrar Codecov para tracking de coverage**
|
|
4. **Crear fixtures de datos de mercado**
|
|
5. **Implementar backtesting automatizado**
|
|
|
|
---
|
|
|
|
**Última actualización:** 2026-01-30
|
|
**Generado por:** Claude Code (Opus 4.5) - Sprint 4
|