Repository navigation
Testing in CI
Two ways to get simulated devices into automated tests: the pytest plugin, or opcua-sim serve
as a background process for any language and test runner.
pip install opcua-sim registers two fixtures. They start a new bench on free ports for every test and
stop it afterwards, so tests cannot leak device state into each other. Starting a server takes about a
second, because asyncua loads the whole standard address space.
import asyncio
import pytest
from asyncua import Client, ua
def read_speed(url): # stands for your code
async def go():
async with Client(url, timeout=2) as client:
idx = await client.get_namespace_index("urn:steitec:opcua-sim")
return await client.get_node(ua.NodeId("drive.Speed", idx)).read_value()
return asyncio.run(go())
def test_reads_the_speed(opcua_bench):
assert read_speed(opcua_bench.url) == 0
def test_notices_a_device_failure(opcua_bench):
opcua_bench.inject("drive", "bad_status", variable="Speed", status="BadDeviceFailure")
with pytest.raises(ua.uaerrors.BadDeviceFailure):
read_speed(opcua_bench.url)opcua_bench is one drive unless you choose a bench with --opcua-bench FILE or the ini key
opcua_bench. For several servers or a specific profile, use the factory:
def test_two_servers(opcua_bench_factory):
plc = opcua_bench_factory("siemens-s7-1500") # a bundled profile
plant = opcua_bench_factory({"devices": [ # or a dict, or a bench file
{"name": "oven", "profile": "temperature-controller"}]})
assert plc.url != plant.urlThe bench API is url, port, control_port, inject, clear, set, get, status and wait_restarted.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.13"
- run: pip install opcua-sim pytest
- run: pytestFor tests in another language, or a system test against a built application:
pip install opcua-sim
opcua-sim serve --bench plant.yaml --port 4840 --control-port 4841 &
sleep 3 # the server takes about a second to start
./run-my-system-tests # connects to opc.tcp://127.0.0.1:4840
opcua-sim inject pump trip code=4 # from a script, or the JSON belowThe control channel is one JSON object per line on the control port, so any language can inject faults:
echo '{"target": "pump", "inject": "trip", "params": {"code": 4}}' | nc -q1 127.0.0.1 4841{"ok": true}The control channel has no authentication. It listens on loopback unless the bench binds to all interfaces.
A client's connection may still be closing when an event loop ends, and asyncio's proactor then warns
about it. If your pytest turns warnings into errors, this fails tests that passed. opcua-sim's own CI
runs on Windows with -p no:unraisableexception for that reason. Linux and macOS are not affected.
opcua-sim · MIT licensed · OPC UA is a trademark of the OPC Foundation. Siemens, Beckhoff, Phoenix Contact, CODESYS, Omron and Leuze are trademarks of their owners; opcua-sim is not affiliated with them.