Files
gogo2/COBY/tests/test_binance_connector.py
2025-08-04 17:28:55 +03:00

341 lines
12 KiB
Python

"""
Tests for Binance exchange connector.
"""
import pytest
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime, timezone
from ..connectors.binance_connector import BinanceConnector
from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel
@pytest.fixture
def binance_connector():
"""Create Binance connector for testing"""
return BinanceConnector()
@pytest.fixture
def sample_binance_orderbook_data():
"""Sample Binance order book data"""
return {
"lastUpdateId": 1027024,
"bids": [
["4.00000000", "431.00000000"],
["3.99000000", "9.00000000"]
],
"asks": [
["4.00000200", "12.00000000"],
["4.01000000", "18.00000000"]
]
}
@pytest.fixture
def sample_binance_depth_update():
"""Sample Binance depth update message"""
return {
"e": "depthUpdate",
"E": 1672515782136,
"s": "BTCUSDT",
"U": 157,
"u": 160,
"b": [
["50000.00", "0.25"],
["49999.00", "0.50"]
],
"a": [
["50001.00", "0.30"],
["50002.00", "0.40"]
]
}
@pytest.fixture
def sample_binance_trade_update():
"""Sample Binance trade update message"""
return {
"e": "trade",
"E": 1672515782136,
"s": "BTCUSDT",
"t": 12345,
"p": "50000.50",
"q": "0.10",
"b": 88,
"a": 50,
"T": 1672515782134,
"m": False,
"M": True
}
class TestBinanceConnector:
"""Test cases for BinanceConnector"""
def test_initialization(self, binance_connector):
"""Test connector initialization"""
assert binance_connector.exchange_name == "binance"
assert binance_connector.websocket_url == BinanceConnector.WEBSOCKET_URL
assert len(binance_connector.message_handlers) >= 3
assert binance_connector.stream_id == 1
assert binance_connector.active_streams == []
def test_normalize_symbol(self, binance_connector):
"""Test symbol normalization"""
# Test standard format
assert binance_connector.normalize_symbol("BTCUSDT") == "BTCUSDT"
# Test with separators
assert binance_connector.normalize_symbol("BTC-USDT") == "BTCUSDT"
assert binance_connector.normalize_symbol("BTC/USDT") == "BTCUSDT"
# Test lowercase
assert binance_connector.normalize_symbol("btcusdt") == "BTCUSDT"
# Test invalid symbol
with pytest.raises(Exception):
binance_connector.normalize_symbol("")
def test_get_message_type(self, binance_connector):
"""Test message type detection"""
# Test depth update
depth_msg = {"e": "depthUpdate", "s": "BTCUSDT"}
assert binance_connector._get_message_type(depth_msg) == "depthUpdate"
# Test trade update
trade_msg = {"e": "trade", "s": "BTCUSDT"}
assert binance_connector._get_message_type(trade_msg) == "trade"
# Test error message
error_msg = {"error": {"code": -1121, "msg": "Invalid symbol"}}
assert binance_connector._get_message_type(error_msg) == "error"
# Test unknown message
unknown_msg = {"data": "something"}
assert binance_connector._get_message_type(unknown_msg) == "unknown"
def test_parse_orderbook_snapshot(self, binance_connector, sample_binance_orderbook_data):
"""Test order book snapshot parsing"""
orderbook = binance_connector._parse_orderbook_snapshot(
sample_binance_orderbook_data,
"BTCUSDT"
)
assert isinstance(orderbook, OrderBookSnapshot)
assert orderbook.symbol == "BTCUSDT"
assert orderbook.exchange == "binance"
assert len(orderbook.bids) == 2
assert len(orderbook.asks) == 2
assert orderbook.sequence_id == 1027024
# Check bid data
assert orderbook.bids[0].price == 4.0
assert orderbook.bids[0].size == 431.0
# Check ask data
assert orderbook.asks[0].price == 4.000002
assert orderbook.asks[0].size == 12.0
@pytest.mark.asyncio
async def test_handle_orderbook_update(self, binance_connector, sample_binance_depth_update):
"""Test order book update handling"""
# Mock callback
callback_called = False
received_data = None
def mock_callback(data):
nonlocal callback_called, received_data
callback_called = True
received_data = data
binance_connector.add_data_callback(mock_callback)
# Handle update
await binance_connector._handle_orderbook_update(sample_binance_depth_update)
# Verify callback was called
assert callback_called
assert isinstance(received_data, OrderBookSnapshot)
assert received_data.symbol == "BTCUSDT"
assert received_data.exchange == "binance"
assert len(received_data.bids) == 2
assert len(received_data.asks) == 2
@pytest.mark.asyncio
async def test_handle_trade_update(self, binance_connector, sample_binance_trade_update):
"""Test trade update handling"""
# Mock callback
callback_called = False
received_data = None
def mock_callback(data):
nonlocal callback_called, received_data
callback_called = True
received_data = data
binance_connector.add_data_callback(mock_callback)
# Handle update
await binance_connector._handle_trade_update(sample_binance_trade_update)
# Verify callback was called
assert callback_called
assert isinstance(received_data, TradeEvent)
assert received_data.symbol == "BTCUSDT"
assert received_data.exchange == "binance"
assert received_data.price == 50000.50
assert received_data.size == 0.10
assert received_data.side == "buy" # m=False means buyer is not maker
assert received_data.trade_id == "12345"
@pytest.mark.asyncio
async def test_subscribe_orderbook(self, binance_connector):
"""Test order book subscription"""
# Mock WebSocket send
binance_connector._send_message = AsyncMock(return_value=True)
# Subscribe
await binance_connector.subscribe_orderbook("BTCUSDT")
# Verify subscription was sent
binance_connector._send_message.assert_called_once()
call_args = binance_connector._send_message.call_args[0][0]
assert call_args["method"] == "SUBSCRIBE"
assert "btcusdt@depth@100ms" in call_args["params"]
assert call_args["id"] == 1
# Verify tracking
assert "BTCUSDT" in binance_connector.subscriptions
assert "orderbook" in binance_connector.subscriptions["BTCUSDT"]
assert "btcusdt@depth@100ms" in binance_connector.active_streams
assert binance_connector.stream_id == 2
@pytest.mark.asyncio
async def test_subscribe_trades(self, binance_connector):
"""Test trade subscription"""
# Mock WebSocket send
binance_connector._send_message = AsyncMock(return_value=True)
# Subscribe
await binance_connector.subscribe_trades("ETHUSDT")
# Verify subscription was sent
binance_connector._send_message.assert_called_once()
call_args = binance_connector._send_message.call_args[0][0]
assert call_args["method"] == "SUBSCRIBE"
assert "ethusdt@trade" in call_args["params"]
assert call_args["id"] == 1
# Verify tracking
assert "ETHUSDT" in binance_connector.subscriptions
assert "trades" in binance_connector.subscriptions["ETHUSDT"]
assert "ethusdt@trade" in binance_connector.active_streams
@pytest.mark.asyncio
async def test_unsubscribe_orderbook(self, binance_connector):
"""Test order book unsubscription"""
# Setup initial subscription
binance_connector.subscriptions["BTCUSDT"] = ["orderbook"]
binance_connector.active_streams.append("btcusdt@depth@100ms")
# Mock WebSocket send
binance_connector._send_message = AsyncMock(return_value=True)
# Unsubscribe
await binance_connector.unsubscribe_orderbook("BTCUSDT")
# Verify unsubscription was sent
binance_connector._send_message.assert_called_once()
call_args = binance_connector._send_message.call_args[0][0]
assert call_args["method"] == "UNSUBSCRIBE"
assert "btcusdt@depth@100ms" in call_args["params"]
# Verify tracking removal
assert "BTCUSDT" not in binance_connector.subscriptions
assert "btcusdt@depth@100ms" not in binance_connector.active_streams
@pytest.mark.asyncio
@patch('aiohttp.ClientSession.get')
async def test_get_symbols(self, mock_get, binance_connector):
"""Test getting available symbols"""
# Mock API response
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value={
"symbols": [
{"symbol": "BTCUSDT", "status": "TRADING"},
{"symbol": "ETHUSDT", "status": "TRADING"},
{"symbol": "ADAUSDT", "status": "BREAK"} # Should be filtered out
]
})
mock_get.return_value.__aenter__.return_value = mock_response
# Get symbols
symbols = await binance_connector.get_symbols()
# Verify results
assert len(symbols) == 2
assert "BTCUSDT" in symbols
assert "ETHUSDT" in symbols
assert "ADAUSDT" not in symbols # Filtered out due to status
@pytest.mark.asyncio
@patch('aiohttp.ClientSession.get')
async def test_get_orderbook_snapshot(self, mock_get, binance_connector, sample_binance_orderbook_data):
"""Test getting order book snapshot"""
# Mock API response
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value=sample_binance_orderbook_data)
mock_get.return_value.__aenter__.return_value = mock_response
# Get order book snapshot
orderbook = await binance_connector.get_orderbook_snapshot("BTCUSDT", depth=20)
# Verify results
assert isinstance(orderbook, OrderBookSnapshot)
assert orderbook.symbol == "BTCUSDT"
assert orderbook.exchange == "binance"
assert len(orderbook.bids) == 2
assert len(orderbook.asks) == 2
def test_get_binance_stats(self, binance_connector):
"""Test getting Binance-specific statistics"""
# Add some test data
binance_connector.active_streams = ["btcusdt@depth@100ms", "ethusdt@trade"]
binance_connector.stream_id = 5
stats = binance_connector.get_binance_stats()
# Verify Binance-specific stats
assert stats['active_streams'] == 2
assert len(stats['stream_list']) == 2
assert stats['next_stream_id'] == 5
# Verify base stats are included
assert 'exchange' in stats
assert 'connection_status' in stats
assert 'message_count' in stats
if __name__ == "__main__":
# Run a simple test
async def simple_test():
connector = BinanceConnector()
# Test symbol normalization
normalized = connector.normalize_symbol("BTC-USDT")
print(f"Symbol normalization: BTC-USDT -> {normalized}")
# Test message type detection
msg_type = connector._get_message_type({"e": "depthUpdate"})
print(f"Message type detection: {msg_type}")
print("Simple Binance connector test completed")
asyncio.run(simple_test())