Skip to content

Testing in CI

Ralf Steinert edited this page Nov 2, 2026 · 1 revision

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.

pytest plugin

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.url

The bench API is url, port, control_port, inject, clear, set, get, status and wait_restarted.

GitHub Actions

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: pytest

As a background process

For 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 below

The 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.

On Windows

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.

Clone this wiki locally