Repository navigation
Testing in CI
Two ways to get simulated devices into automated tests: the pytest plugin
(modbus-sim installed in the test environment) or modbus-sim serve as a
background process (any language, any test runner).
pip install modbus-sim registers two fixtures. They start a fresh bench on
free ports for every test and stop it afterwards, so tests cannot leak device
state into each other and can run in parallel.
from pymodbus.client import ModbusTcpClient
def test_reads_the_meter(modbus_bench):
c = ModbusTcpClient("127.0.0.1", port=modbus_bench.port)
c.connect()
r = c.read_input_registers(0, count=2, device_id=1)
assert 220 < c.convert_from_registers(r.registers, c.DATATYPE.FLOAT32) < 240
c.close()pytest --modbus-bench examples/real-devices.yaml- Which bench
modbus_benchstarts:--modbus-bench FILE, or the ini keymodbus_bench = FILEinpytest.ini/[tool.pytest.ini_options](relative to the rootdir). Without either: onevfdat unit 1. -
modbus_bench.portis the TCP port,modbus_bench.client()a connectedmodbus_sim.client.ModbusClient. -
modbus_bench.inject(device, kind, **params)and.clear(device)switch faults;deviceis the name from the bench file or the unit ID. -
modbus_bench.value(device, point)reads a point's current value without a Modbus request — handy for asserting what your code wrote. -
modbus_bench_factory(source, **options)starts more benches in one test: a bench file, a dict, or a bundled profile name;framing="rtu"for RTU over TCP.
import pytest
BENCH = {"devices": [
{"profile": "eastron-sdm630", "unit": 1, "name": "meter"},
{"profile": "omron-e5cc", "unit": 2, "name": "controller"},
]}
@pytest.fixture
def plant(modbus_bench_factory):
return modbus_bench_factory(BENCH)
def test_controller_set_point(plant):
from mylab.oven import Oven # your code
Oven("127.0.0.1", plant.port, unit=2).set_point(150)
assert plant.value("controller", "set_point") == 150Device models run on the real clock. Tests that need a process to move (a controller heating, an inverter going through a day, energy counting) should speed the model up instead of sleeping long:
{"profile": "omron-e5cc", "unit": 2, "model_params": {"time_scale": 100}}With the plugin there is nothing to set up beyond installing the package:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install modbus-sim pymodbus pytest -e .
- run: pytest --modbus-bench tests/bench.yamlFor code that is not tested with pytest — a Node-RED flow, a Go or C# service, an integration test that starts your gateway — run the simulator as a background process and wait until it answers:
- run: pip install modbus-sim
- name: Start simulated devices
run: |
modbus-sim serve --bench tests/bench.yaml --port 5020 > sim.log 2>&1 &
for i in $(seq 50); do modbus-sim list >/dev/null 2>&1 && break; sleep 0.2; done
- run: ./run-integration-tests.sh # your tests, against 127.0.0.1:5020
- if: always()
run: cat sim.logmodbus-sim list asks the control port and fails until the bench is up. In
the tests, switch faults with modbus-sim inject <device> <fault> (the control
port listens on 127.0.0.1:5021).
On Linux runners, --pty gives a virtual serial port without hardware or
extra packages:
def test_rtu_master(modbus_bench_factory):
bench = modbus_bench_factory("eastron-sdm630", pty=True)
port = bench.path # e.g. /dev/pts/3
... # open it with your RTU masterBaud rate and parity are not simulated over a pty. On Windows runners use a
virtual null-modem pair (com0com) and --serial COMx.
modbus-sim · MIT licensed · Eastron, Omron and SunSpec are trademarks of their owners; modbus-sim is not affiliated with them.