Skip to content

Test Suite Setup

This page covers the one-time configuration needed to run function block tests with pytest.


Dependencies

Add test dependencies to your pyproject.toml:

[project.optional-dependencies]
test = [
    "pytest>=8.0",
    "pytest-asyncio>=0.24",
    "neops-remote-lab",        # only if using remote lab tests
]

Install with:

uv sync --extra test

pytest Configuration

Add a [tool.pytest.ini_options] section to pyproject.toml:

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
markers = [
    "function_block: marks tests as function block tests",
]

Why asyncio_mode = "auto"?

With "auto", every async def test runs as async automatically — no need to add @pytest.mark.asyncio to each function. The SDK decorators (@fb_test_case, @fb_test_case_with_lab) also handle this internally, so you get consistent behavior whether you write standalone tests or use the decorator shortcut.

Mode Behavior Trade-off
auto (recommended) All async def tests run as async automatically Less boilerplate; matches the SDK's own config
strict You must add @pytest.mark.asyncio to every async test Explicit — but easy to forget and get coroutine never awaited errors

conftest.py Patterns

A typical conftest.py for function block projects:

import pytest
from neops_workflow_engine_client import DeviceTypeDto
from neops_worker_sdk.testing.factories.context_factory import create_workflow_context


@pytest.fixture
def cisco_device() -> DeviceTypeDto:
    return DeviceTypeDto(
        id=1,
        hostname="core-rtr-01",
        ip="10.0.1.1",
        platform="ios",
        username="admin",
        password="secret",
    )


@pytest.fixture
def device_context(cisco_device: DeviceTypeDto):
    return create_workflow_context(
        run_on="device",
        entity_id=cisco_device.id,
        devices=[cisco_device],
    )

create_workflow_context builds a WorkflowContext from raw DTOs — the same object your function block receives in run().

Parameter Type Purpose
run_on str Entity type: "device", "group", "interface", or "global"
entity_id int \| float \| None ID of the primary entity the block runs on
devices Iterable[DeviceTypeDto] Devices available in the context
device_groups Iterable[DeviceGroupTypeDto] Groups available in the context
interfaces Iterable[InterfaceTypeDto] Interfaces available in the context

Environment Variables

Variable Purpose Default
BLOCKING_DETECTION_THRESHOLD Seconds before the blocking detector warns 0.5
REMOTE_LAB_URL Base URL of the remote lab server (unset — uses local labs)

Running Tests

The test suite uses markers to separate tests into tiers. By default, pytest runs unit tests and example function block tests — only remote lab tests are excluded so you can iterate locally without network dependencies.

Test Tiers

Marker What it covers When it runs
(none) SDK unit tests in tests/ Always (default)
examples Example function block tests in examples/ Always (default)
function_block Function block lifecycle tests via @fb_test_case Always (default)
remote_lab Integration tests against real devices via @fb_test_case_with_lab Opt-in: make test-function-blocks (requires REMOTE_LAB_URL)

Commands

uv run pytest                                             # unit tests + examples (default)
uv run pytest -m "examples and not remote_lab"            # example tests only
uv run pytest -m "function_block or remote_lab"           # integration tests (needs REMOTE_LAB_URL)
uv run pytest -m ""                                       # everything
uv run pytest -k "test_config_backup"                     # by name pattern

Or use the Make targets:

make test               # unit tests + examples (default)
make test-examples      # example tests only
make test-function-blocks   # function block + remote lab tests
make test-all           # everything

Running all tests locally

To run the full suite including remote lab tests, set the lab URL first:

export REMOTE_LAB_URL=http://<remote-lab-host>:8000
make test-all

Marker Configuration

Markers are defined in pyproject.toml. The default addopts excludes only remote_lab tests:

[tool.pytest.ini_options]
addopts = ["-m", "not remote_lab", "--ignore=docs"]

To override the default exclusion for a single run, pass -m explicitly — it replaces the default:

uv run pytest -m ""          # everything including remote lab
uv run pytest -m "remote_lab" # remote lab tests only

How markers are applied

  • @fb_test_case automatically marks tests with function_block.
  • @fb_test_case_with_lab automatically marks tests with both function_block and remote_lab.
  • Tests collected from examples/ are automatically marked with examples via the root conftest.py.

Linting Tests

If you use ruff, ensure test files are not excluded:

[tool.ruff]
extend-include = ["tests/**/*.py"]