Files
crypto_trader/docs/developer/testing.md

9.3 KiB

Testing Guide

This guide covers testing practices for Crypto Trader, including backend API testing, integration testing, and end-to-end testing.

Test Structure

Tests are organized to mirror the source code structure:

tests/
├── unit/              # Unit tests
│   ├── backend/      # Backend API tests
│   ├── core/         # Core module tests
│   └── ...
├── integration/       # Integration tests
├── e2e/              # End-to-end tests
├── fixtures/         # Test fixtures
├── utils/            # Test utilities
└── performance/      # Performance benchmarks

Running Tests

All Tests

pytest

With Coverage

pytest --cov=src --cov-report=html

Specific Test File

pytest tests/unit/core/test_config.py

Specific Test

pytest tests/unit/core/test_config.py::test_config_loading

Verbose Output

pytest -v

Test Categories

# Unit tests only
pytest -m unit

# Integration tests only
pytest -m integration

# End-to-end tests only
pytest -m e2e

Writing Tests

Unit Tests

Test individual functions and classes in isolation:

import pytest
from unittest.mock import Mock, patch
from src.core.config import get_config

class TestConfig:
    """Tests for configuration system."""
    
    def test_config_loading(self):
        """Test configuration loading."""
        config = get_config()
        assert config is not None
        assert config.config_dir is not None

Backend API Tests

Test FastAPI endpoints using TestClient:

from fastapi.testclient import TestClient
from backend.main import app

client = TestClient(app)

def test_get_orders():
    """Test getting orders endpoint."""
    response = client.get("/api/trading/orders")
    assert response.status_code == 200
    data = response.json()
    assert isinstance(data, list)

def test_place_order():
    """Test placing an order."""
    order_data = {
        "exchange_id": 1,
        "symbol": "BTC/USD",
        "side": "buy",
        "type": "market",
        "quantity": 0.1,
        "paper_trading": True
    }
    response = client.post("/api/trading/orders", json=order_data)
    assert response.status_code == 201
    data = response.json()
    assert data["symbol"] == "BTC/USD"

Integration Tests

Test component interactions:

import pytest
from fastapi.testclient import TestClient
from backend.main import app

@pytest.mark.integration
def test_trading_workflow(client: TestClient):
    """Test complete trading workflow."""
    # Place order
    order_response = client.post("/api/trading/orders", json={...})
    assert order_response.status_code == 201
    order_id = order_response.json()["id"]
    
    # Check order status
    status_response = client.get(f"/api/trading/orders/{order_id}")
    assert status_response.status_code == 200
    
    # Check portfolio update
    portfolio_response = client.get("/api/portfolio/current")
    assert portfolio_response.status_code == 200

End-to-End Tests

Test complete user workflows:

@pytest.mark.e2e
async def test_paper_trading_scenario():
    """Test complete paper trading scenario."""
    # Test full application flow through API
    pass

Test Fixtures

Use fixtures for common setup:

import pytest
from fastapi.testclient import TestClient
from backend.main import app
from src.core.database import get_database

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

@pytest.fixture
def mock_exchange():
    """Mock exchange adapter."""
    exchange = Mock()
    exchange.fetch_balance.return_value = {'USD': {'free': 1000}}
    return exchange

@pytest.fixture
def test_db():
    """Test database fixture."""
    # Use in-memory SQLite for unit tests (fast, isolated)
    # Note: Requires aiosqlite installed as test dependency
    db = get_database()
    # Setup test data
    yield db
    # Cleanup

Mocking

Mocking External APIs

from unittest.mock import patch, AsyncMock

@patch('src.exchanges.coinbase.ccxt')
async def test_coinbase_connection(mock_ccxt):
    """Test Coinbase connection."""
    mock_exchange = AsyncMock()
    mock_ccxt.coinbaseadvanced.return_value = mock_exchange
    mock_exchange.load_markets = AsyncMock()
    
    adapter = CoinbaseExchange(...)
    await adapter.connect()
    
    assert adapter.is_connected

Mocking Database

@pytest.fixture
def test_db():
    """Create test database."""
    # Use in-memory SQLite for unit tests (internal use only)
    from src.core.database import Database
    db = Database("sqlite:///:memory:")
    db.create_tables()
    return db

Mocking Services

@pytest.fixture
def mock_trading_engine():
    """Mock trading engine."""
    engine = Mock()
    engine.execute_order.return_value = MockOrder(id=1, status="filled")
    return engine

def test_place_order_endpoint(mock_trading_engine):
    """Test order placement with mocked engine."""
    with patch('backend.api.trading.get_trading_engine', return_value=mock_trading_engine):
        response = client.post("/api/trading/orders", json={...})
        assert response.status_code == 201

Async Testing

Use pytest-asyncio for async tests:

import pytest

@pytest.mark.asyncio
async def test_async_function():
    """Test async function."""
    result = await my_async_function()
    assert result is not None

WebSocket Testing

Test WebSocket endpoints:

from fastapi.testclient import TestClient

def test_websocket_connection(client: TestClient):
    """Test WebSocket connection."""
    with client.websocket_connect("/ws/") as websocket:
        # Send message
        websocket.send_json({"type": "subscribe", "channel": "prices"})
        
        # Receive message
        data = websocket.receive_json()
        assert data["type"] == "price_update"

Test Coverage

Target 95% code coverage:

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

# View in browser
open htmlcov/index.html

Coverage Configuration

Configure in pytest.ini:

[pytest]
cov = src
cov-report = term-missing
cov-report = html
cov-fail-under = 95

Frontend Testing

The frontend uses React Testing Library and Vitest for testing. See Frontend Testing Guide for detailed information.

Quick Start

cd frontend
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event vitest jsdom
npm test

Component Testing

import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { describe, it, expect, vi } from 'vitest';
import StrategiesPage from '../pages/StrategiesPage';
import * as strategiesApi from '../api/strategies';

vi.mock('../api/strategies');

describe('StrategiesPage', () => {
  it('renders and displays strategies', async () => {
    const mockStrategies = [{ id: 1, name: 'Test Strategy', ... }];
    vi.mocked(strategiesApi.strategiesApi.listStrategies).mockResolvedValue(mockStrategies);

    const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
    
    render(
      <QueryClientProvider client={queryClient}>
        <StrategiesPage />
      </QueryClientProvider>
    );

    await waitFor(() => {
      expect(screen.getByText('Test Strategy')).toBeInTheDocument();
    });
  });
});

Testing New Components

All new components should have corresponding test files:

  • StrategiesPageStrategiesPage.test.tsx
  • TradingPageTradingPage.test.tsx
  • StrategyDialogStrategyDialog.test.tsx
  • OrderFormOrderForm.test.tsx
  • And all other new components

See Frontend Testing Guide for comprehensive testing patterns.

Best Practices

  1. Test Independence: Tests should not depend on each other
  2. Fast Tests: Unit tests should run quickly (< 1 second each)
  3. Clear Names: Test names should describe what they test
  4. One Assertion: Prefer one assertion per test when possible
  5. Mock External: Mock external dependencies (APIs, databases)
  6. Test Edge Cases: Test boundary conditions and errors
  7. Documentation: Document complex test scenarios
  8. Arrange-Act-Assert: Structure tests clearly
  9. Use Fixtures: Reuse common setup code
  10. Isolation: Each test should be able to run independently

Test Organization

Unit Tests

  • Test single function/class
  • Mock external dependencies
  • Fast execution
  • High coverage

Integration Tests

  • Test component interactions
  • Use test database
  • Test real workflows
  • Moderate speed

E2E Tests

  • Test complete user flows
  • Use test environment
  • Slow execution
  • Critical paths only

Continuous Integration

Tests run automatically in CI/CD:

  • All tests must pass
  • Coverage must meet threshold (95%)
  • Code style must pass
  • Type checking must pass (if using mypy)

Debugging Tests

Verbose Output

pytest -vv  # Very verbose
pytest -s   # Show print statements

Debugging Failed Tests

# Drop into debugger on failure
pytest --pdb

# Drop into debugger on first failure
pytest -x --pdb

Running Last Failed Tests

pytest --lf  # Last failed
pytest --ff  # Failed first