trading-platform/docs/95-guias-desarrollo/TESTING-STRATEGY.md
Adrian Flores Cortes 6dce71d5d2 [TASK-2026-01-30-ANALISIS-INTEGRACION] docs: Add DATABASE-SCHEMA.md and TESTING-STRATEGY.md
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>
2026-01-30 15:55:22 -06:00

14 KiB

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:

// 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:

// 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:

# 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:

// setup/test-db.ts
beforeAll(async () => {
  await setupTestDatabase();
});

afterAll(async () => {
  await teardownTestDatabase();
});

beforeEach(async () => {
  await clearTables();
});

Ejemplo:

// 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

// 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

# 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

// 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

# 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

// 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

// 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

// __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

# 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

# .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

# .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

// 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

# 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

# Unit tests
npm run test:unit

# Integration tests
npm run test:integration

# Coverage
npm run test:coverage

Frontend

# Unit tests
npm run test

# Watch mode
npm run test:watch

# Coverage
npm run test:coverage

ML Engine

# All tests
pytest

# Unit only
pytest tests/unit/

# With coverage
pytest --cov=src --cov-report=html

E2E

# 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