Testing Guide¶
Comprehensive guide to testing with the Bluetooth SIG Standards Library.
Running Tests¶
Run All Tests¶
python -m pytest tests/ -v
Run with Coverage¶
python -m pytest tests/ --cov=src/bluetooth_sig --cov-report=html --cov-report=term-missing
Run Specific Tests¶
# Run tests for a specific module
python -m pytest tests/test_battery_level.py -v
# Run a specific test
python -m pytest tests/test_battery_level.py::TestBatteryLevel::test_decode_valid -v
# Run tests matching a pattern
python -m pytest tests/ -k "battery" -v
Testing Without Hardware¶
One of the key advantages of this library’s architecture is that you can test BLE data parsing without any BLE hardware.
Unit Testing Example¶
import pytest
from bluetooth_sig import BluetoothSIGTranslator
class TestBLEParsing:
"""Test BLE characteristic parsing without hardware."""
def test_battery_level_parsing(self):
"""Test battery level parsing with mock data."""
# ============================================
# SIMULATED DATA - For testing without hardware
# ============================================
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
mock_data = bytearray([75]) # 75% battery
translator = BluetoothSIGTranslator()
# Parse
result = translator.parse_characteristic(BATTERY_LEVEL_UUID, mock_data)
# Assert
assert result.value == 75
assert 0 <= result.value <= 100
def test_temperature_parsing(self):
"""Test temperature parsing with mock data."""
# ============================================
# SIMULATED DATA - For testing without hardware
# ============================================
TEMP_UUID = "2A6E" # Temperature characteristic UUID
mock_data = bytearray([0x64, 0x09]) # 24.04°C
translator = BluetoothSIGTranslator()
result = translator.parse_characteristic(TEMP_UUID, mock_data)
assert result.value == 24.04
assert isinstance(result.value, float)
Testing Error Conditions¶
import pytest
from bluetooth_sig.gatt.exceptions import (
InsufficientDataError,
ValueRangeError,
)
class TestErrorHandling:
"""Test error handling without hardware."""
def test_insufficient_data(self):
"""Test error when data is too short."""
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
translator = BluetoothSIGTranslator()
# Empty data
with pytest.raises(InsufficientDataError):
translator.parse_characteristic(BATTERY_LEVEL_UUID, bytearray([]))
def test_out_of_range_value(self):
"""Test error when value is out of range."""
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
translator = BluetoothSIGTranslator()
# Battery level > 100%
with pytest.raises(ValueRangeError):
translator.parse_characteristic(
BATTERY_LEVEL_UUID, bytearray([150])
)
Mocking BLE Interactions¶
When integrating with BLE libraries, you can mock the BLE operations:
Mocking bleak¶
from unittest.mock import AsyncMock, patch
import pytest
from bluetooth_sig import BluetoothSIGTranslator
@pytest.fixture
def mock_bleak_client():
"""Mock BleakClient for testing."""
with patch("bleak.BleakClient") as mock:
client = AsyncMock()
mock.return_value.__aenter__.return_value = client
yield client
@pytest.mark.asyncio
async def test_read_battery_with_mock(mock_bleak_client):
"""Test reading battery level with mocked BLE."""
# ============================================
# TEST SETUP - Mocked BLE data
# ============================================
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
mock_battery_data = bytearray([85]) # 85% battery
# Setup mock
mock_bleak_client.read_gatt_char.return_value = mock_battery_data
# Your application code
translator = BluetoothSIGTranslator()
raw_data = await mock_bleak_client.read_gatt_char(BATTERY_LEVEL_UUID)
result = translator.parse_characteristic(BATTERY_LEVEL_UUID, raw_data)
# Assert
assert result.value == 85
mock_bleak_client.read_gatt_char.assert_called_once_with(
BATTERY_LEVEL_UUID
)
Mocking simplepyble¶
from unittest.mock import Mock, patch
def test_read_battery_simplepyble_mock():
"""Test reading battery with mocked simplepyble."""
# ============================================
# TEST SETUP - Mocked BLE data
# ============================================
SERVICE_UUID = "180F" # Battery Service
BATTERY_LEVEL_UUID = "2A19" # Battery Level characteristic
mock_battery_data = bytes([75]) # 75% battery
# Create mock peripheral
mock_peripheral = Mock()
mock_peripheral.read.return_value = mock_battery_data
# Your application code
translator = BluetoothSIGTranslator()
raw_data = mock_peripheral.read(SERVICE_UUID, BATTERY_LEVEL_UUID)
result = translator.parse_characteristic(
BATTERY_LEVEL_UUID, bytearray(raw_data)
)
# Assert
assert result.value == 75
mock_peripheral.read.assert_called_once()
Test Data Generation¶
Creating Test Vectors¶
class TestDataFactory:
"""Factory for creating test data."""
@staticmethod
def battery_level(percentage: int) -> bytearray:
"""Create battery level test data."""
assert 0 <= percentage <= 100
return bytearray([percentage])
@staticmethod
def temperature(celsius: float) -> bytearray:
"""Create temperature test data."""
# Temperature encoded as sint16 with 0.01°C resolution
value = int(celsius * 100)
return bytearray(value.to_bytes(2, byteorder="little", signed=True))
@staticmethod
def humidity(percentage: float) -> bytearray:
"""Create humidity test data."""
# Humidity encoded as uint16 with 0.01% resolution
value = int(percentage * 100)
return bytearray(value.to_bytes(2, byteorder="little", signed=False))
# Usage
def test_with_factory():
# ============================================
# TEST DATA - From factory helpers
# ============================================
BATTERY_UUID = "2A19"
TEMP_UUID = "2A6E"
HUMIDITY_UUID = "2A6F"
translator = BluetoothSIGTranslator()
# Generate test data
battery_data = TestDataFactory.battery_level(85)
temp_data = TestDataFactory.temperature(24.04)
humidity_data = TestDataFactory.humidity(49.22)
# Test parsing
assert (
translator.parse_characteristic(BATTERY_UUID, battery_data).value == 85
)
assert translator.parse_characteristic(TEMP_UUID, temp_data).value == 24.04
assert (
translator.parse_characteristic(HUMIDITY_UUID, humidity_data).value
== 49.22
)
Parametrized Testing¶
Test multiple scenarios efficiently:
import pytest
@pytest.mark.parametrize(
"battery_level,expected",
[
(0, 0),
(25, 25),
(50, 50),
(75, 75),
(100, 100),
],
)
def test_battery_levels(battery_level, expected):
"""Test various battery levels."""
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
translator = BluetoothSIGTranslator()
data = bytearray([battery_level])
result = translator.parse_characteristic(BATTERY_LEVEL_UUID, data)
assert result.value == expected
@pytest.mark.parametrize(
"invalid_data",
[
bytearray([]), # Too short
bytearray([101]), # Too high
bytearray([255]), # Way too high
],
)
def test_invalid_battery_data(invalid_data):
"""Test error handling for invalid data."""
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
translator = BluetoothSIGTranslator()
with pytest.raises((InsufficientDataError, ValueRangeError)):
translator.parse_characteristic(BATTERY_LEVEL_UUID, invalid_data)
Testing with Fixtures¶
Pytest Fixtures¶
import pytest
from bluetooth_sig import BluetoothSIGTranslator
@pytest.fixture
def translator():
"""Provide a translator instance."""
return BluetoothSIGTranslator()
@pytest.fixture
def valid_battery_data():
"""Provide valid battery level data."""
return bytearray([75])
@pytest.fixture
def valid_temp_data():
"""Provide valid temperature data."""
return bytearray([0x64, 0x09]) # 24.04°C
def test_with_fixtures(translator, valid_battery_data):
"""Test using fixtures."""
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
result = translator.parse_characteristic(
BATTERY_LEVEL_UUID, valid_battery_data
)
assert result.value == 75
Integration Testing¶
Test complete workflows:
class TestIntegration:
"""Integration tests for complete workflows."""
def test_multiple_characteristics(self):
"""Test parsing multiple characteristics."""
# ============================================
# SIMULATED DATA - Multiple sensor readings
# ============================================
BATTERY_UUID = "2A19"
TEMP_UUID = "2A6E"
HUMIDITY_UUID = "2A6F"
translator = BluetoothSIGTranslator()
# Simulate reading multiple characteristics
sensor_data = {
BATTERY_UUID: bytearray([85]), # Battery: 85%
TEMP_UUID: bytearray([0x64, 0x09]), # Temp: 24.04°C
HUMIDITY_UUID: bytearray([0x3A, 0x13]), # Humidity: 49.22%
}
results = {}
for uuid, data in sensor_data.items():
results[uuid] = translator.parse_characteristic(uuid, data)
# Verify all parsed correctly
assert results[BATTERY_UUID].value == 85
assert results[TEMP_UUID].value == 24.04
assert results[HUMIDITY_UUID].value == 49.22
def test_uuid_resolution_workflow(self):
"""Test UUID resolution workflow."""
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
translator = BluetoothSIGTranslator()
# Resolve UUID to name
char_info = translator.get_sig_info_by_uuid(BATTERY_LEVEL_UUID)
assert char_info.name == "Battery Level"
# Resolve name to UUID
battery_uuid = translator.get_sig_info_by_name("Battery Level")
assert battery_uuid.uuid == BATTERY_LEVEL_UUID
# Round-trip
assert (
translator.get_sig_info_by_uuid(battery_uuid.uuid).name
== char_info.name
)
Performance Testing¶
import time
def test_parsing_performance():
"""Test parsing performance."""
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
data = bytearray([75]) # Test data
translator = BluetoothSIGTranslator()
# Warm up
for _ in range(100):
translator.parse_characteristic(BATTERY_LEVEL_UUID, data)
# Measure
start = time.perf_counter()
iterations = 10000
for _ in range(iterations):
translator.parse_characteristic(BATTERY_LEVEL_UUID, data)
elapsed = time.perf_counter() - start
# Should be fast (< 100μs per parse)
avg_time = elapsed / iterations
assert avg_time < 0.0001, (
f"Parsing too slow: {avg_time:.6f}s per iteration"
)
print(f"Average parse time: {avg_time * 1000000:.1f}μs")
Test Organization¶
Recommended test structure:
tests/
├── conftest.py # Shared fixtures
├── test_core/
│ ├── test_translator.py # Core API tests
│ └── test_uuid_resolution.py # UUID resolution tests
├── test_characteristics/
│ ├── test_battery_level.py # Battery characteristic
│ ├── test_temperature.py # Temperature characteristic
│ └── test_humidity.py # Humidity characteristic
├── test_services/
│ └── test_battery_service.py # Service tests
├── test_registry/
│ └── test_uuid_registry.py # Registry tests
└── test_integration/
└── test_workflows.py # End-to-end tests
Continuous Integration¶
Example GitHub Actions workflow:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -e ".[dev,test]"
- name: Run tests
run: |
pytest tests/ --cov=src/bluetooth_sig --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
Best Practices¶
1. Test One Thing at a Time¶
# ✅ Good - tests one aspect
def test_battery_valid_value():
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
translator = BluetoothSIGTranslator()
result = translator.parse_characteristic(
BATTERY_LEVEL_UUID, bytearray([75])
)
assert result.value == 75
def test_battery_invalid_value():
BATTERY_LEVEL_UUID = "2A19" # UUID from BLE spec
translator = BluetoothSIGTranslator()
with pytest.raises(ValueRangeError):
translator.parse_characteristic(BATTERY_LEVEL_UUID, bytearray([150]))
# ❌ Bad - tests multiple things
def test_battery_everything():
translator = BluetoothSIGTranslator()
# Tests too many scenarios in one test
...
2. Use Descriptive Names¶
# ✅ Good
def test_battery_level_rejects_value_above_100(): ...
# ❌ Bad
def test_battery_1(): ...
3. Arrange-Act-Assert Pattern¶
def test_temperature_parsing():
# Arrange
TEMP_UUID = "2A6E" # Temperature characteristic UUID
data = bytearray([0x64, 0x09]) # 24.04°C
translator = BluetoothSIGTranslator()
# Act
result = translator.parse_characteristic(TEMP_UUID, data)
# Assert
assert result.value == 24.04
Next Steps¶
Contributing Guide - Contribute to the project
API Reference - API documentation
Examples - More examples