Step 2: Running Without Hardware #

Goal: Run your tests without real instruments by wrapping your driver classes with Litmus's Mock factory.

In step 1 you wrote vanilla pytest tests against psu and dmm fixtures defined in conftest.py. This step shows the smallest change that lets the same tests run on a laptop with no hardware attached.

The conftest pattern #

Your conftest.py already returns driver instances. Wrap them in litmus.instruments.mocks.Mock when the --mock-instruments flag is set:

# tests/conftest.py
import pytest
from drivers import DMM, PSU
from litmus.instruments.mocks import Mock
 
 
@pytest.fixture(scope="session")
def psu(mock_instruments) -> PSU:
    if mock_instruments:
        return Mock(PSU, measure_voltage=5.0, measure_current=0.042)
    return PSU(resource="TCPIP::192.168.1.101::INSTR")
 
 
@pytest.fixture(scope="session")
def dmm(mock_instruments) -> DMM:
    if mock_instruments:
        return Mock(DMM, measure_dc_voltage=3.31)
    return DMM(resource="TCPIP::192.168.1.102::INSTR")

mock_instruments is a fixture Litmus provides — it returns True whenever --mock-instruments is on the command line or LITMUS_MOCK_INSTRUMENTS=1 is set.

Mock(DMM, measure_dc_voltage=3.31) returns an object that quacks like a DMM — every method call is a no-op unless you configure a return value. Pass a literal for a constant return, a dict to map argument values, or a callable for dynamic behavior.

This is exactly what examples/01-vanilla and examples/02-verify ship.

Running with mocks #

pytest tests/ --mock-instruments -v

Same test code as step 1, no hardware required.

# Or via env var
LITMUS_MOCK_INSTRUMENTS=1 pytest tests/ -v

Mock factory cheat sheet #

# Constant return
dmm = Mock(DMM, measure_dc_voltage=3.31)
 
# Map by argument — different return per query string
dmm = Mock(DMM, query={"MEAS:VOLT:DC?": "3.300", "*IDN?": "Keysight,34461A,..."})
 
# Callable — for noise or sweeps
import random
dmm = Mock(DMM, measure_dc_voltage=lambda: 3.3 + random.gauss(0, 0.005))

Every method not configured is a silent no-op. Reading an unconfigured attribute (not a method call) raises AttributeError — that's the seam where a missing mock spec shows up.

Mocks vs real hardware #

You runmock_instruments isTest code
pytest tests/Falseidentical
pytest tests/ --mock-instrumentsTrueidentical

The point of the wrap-in-conftest pattern: the test code is the same on a laptop and on the bench. Tests don't know which mode they're in.

What you learned #

  • --mock-instruments flag + the mock_instruments fixture
  • Mock(DriverClass, method=return_value, ...) wraps any driver class
  • The conftest fixture decides real vs mock — tests don't change

In later steps you'll lift this conftest conditional into station YAML (step 7) so the same setup serves a whole bench of tests. For now, conftest is enough.

Continue #

Now let's adopt three of Litmus's per-test fixturescontext, verify, logger — to start recording measurements with limits.

Step 1: Run Something | Step 3: pytest-native tests →

Tutorial · Step 3 of 11