Overview
Philosophy
micropython-mock-machine is designed with the following principles:
- Drop-in Replacement: The mock should behave as close to the real
machinemodule as possible - Testability First: Provide features that make testing easier, even if they don't exist in real hardware
- Predictable Behavior: Mock behavior should be deterministic and controllable
- Zero Hardware Required: All testing should be possible on any Python environment
Architecture
Module Registration
The core mechanism is replacing the machine module in Python's module system:
This is wrapped in the convenient register_as_machine() function. Once called, any subsequent import machine will load the mock module instead.
Mock Object Lifecycle
Mock objects maintain state throughout their lifecycle:
# Pin objects are singleton-like per pin number
pin1 = machine.Pin(0)
pin2 = machine.Pin(0)
assert pin1 is pin2 # Same object
# I2C devices are added to buses
i2c = machine.I2C(0)
device = I2CDevice(addr=0x50, i2c=i2c)
assert 0x50 in i2c.scan() # Device is visible
State Management
Each mock object manages its own state:
- Pins: Value, mode, pull resistor, interrupt handlers
- I2C: Connected devices, register values
- SPI: Read/write buffers, transaction history
- ADC: Simulated analog values
- Timers: Callback functions, periods
- UART: Input/output buffers
Testing Workflow
A typical testing workflow with mock_machine:
-
Setup Phase
-
Configuration Phase
-
Test Execution
-
Verification
Key Differences from Real Hardware
While mock_machine strives for compatibility, some differences exist:
Enhanced Testability Features
-
Direct State Access: You can directly read/write internal state
-
Device Addition: I2C/SPI can have devices dynamically added
-
Transaction History: SPI tracks all transactions
Simplified Behavior
- No Timing: Operations are instantaneous
- No Hardware Limits: Unlimited devices, pins, etc.
- Perfect Reliability: No communication errors unless simulated
Integration with Test Frameworks
unittest
import unittest
import mock_machine
class TestMyDevice(unittest.TestCase):
def setUp(self):
mock_machine.register_as_machine()
def test_device(self):
# Your tests here
pass
pytest
import pytest
import mock_machine
@pytest.fixture(autouse=True)
def mock_hardware():
mock_machine.register_as_machine()
yield
mock_machine.Pin.pins.clear()
asyncio
import asyncio
async def test_async_operation():
# Async test code
await asyncio.sleep(0)
# Run with asyncio
asyncio.run(test_async_operation())
Performance Considerations
mock_machine is designed for testing, not performance:
- Operations are synchronous (except where asyncio is used)
- No optimization for large data transfers
- Memory usage grows with stored state
For performance testing, consider: - Limiting transaction history - Clearing state between tests - Using minimal mock features needed
Next Steps
- Learn about Basic Usage
- Explore Testing Patterns
- See Advanced Topics