Testing your MUSCLE3 model

When developing a MUSCLE3 component, it is useful to be able to test it in isolation — without having to set up and run the full coupled simulation. The MuscleTester class (together with the muscle3_tester pytest fixture) makes this easy: it starts a real MUSCLE3 manager in the background and connects a tester component to all ports of your implementation, so you can send and receive messages programmatically from within a pytest test. This means you can script the entire testing procedure and run it automatically — for example, as part of a continuous integration pipeline — so that any time you change your component, you can immediately verify that it still behaves correctly, without having to test it by hand.

Note

The testing infrastructure described here relies on pytest. Tests are written as ordinary Python functions whose names start with test_, and pytest discovers and runs them automatically.

A fixture is a reusable helper that pytest prepares before a test and cleans up afterwards. You request a fixture simply by adding a parameter with the same name to your test function.

Quick start

Step 1: Provide the yMMSL configuration for your implementation

start_implementation() accepts the yMMSL in two forms.

Place the yMMSL file in a tests/ folder at the root of your project, next to your test files:

my_project/
├── micro.py
├── micro.ymmsl
└── tests/
    └── test_micro.py

For example, for a simple micro model that receives a value on init and sends a result on final:

tests/micro.ymmsl
ymmsl_version: v0.2

programs:
  micro:
    ports:
      f_init: init
      o_f: final
    executable: python3
    args: /path/to/my_project/micro.py

Step 2: Use the muscle3_tester fixture in your test

tests/test_micro.py
from pathlib import Path
from libmuscle import Message
from libmuscle.pytest import MuscleTester

PROJECT_DIR = Path(__file__).resolve().parents[1]


def test_micro_model(muscle3_tester: MuscleTester) -> None:
    """Test the micro model by acting as the macro."""
    tester = muscle3_tester.start_implementation(
        PROJECT_DIR / "micro.ymmsl", "micro"
        )

    # Send a message to the micro model's 'init' port
    tester.send("init", Message(0.0, 10.0, 42))

    # Receive the result from the micro model's 'final' port
    reply = tester.receive("final")

    assert reply.data == 42

Step 3: Run your tests

Run pytest from the project root so that all paths resolve correctly:

cd my_project
pytest

Timeouts and error handling

By default, start_implementation() uses a 60-second timeout for all receive operations. If the implementation does not send a message within that time, a RuntimeError is raised and the test fails.

You may want to increase the timeout when your implementation performs expensive work between messages, for example, a micro-model that runs a numerical solver for several minutes per time step. In those cases the default 60 seconds may expire before the component has had a chance to reply, causing a spurious test failure.

Conversely, you may want to decrease the timeout in fast unit tests so that a missing send call is detected quickly rather than making the test suite hang for a full minute.

You can adjust this timeout:

tester = muscle3_tester.start_implementation(
    PROJECT_DIR / "micro.ymmsl", "micro", default_timeout=5.0
)

You can also override the timeout for individual receive calls:

reply = tester.receive("final", timeout=2.0)