Experiment tests do not need Gate d1 (that applies to device modules only). Instead, mock the device clients to test measurement logic without connecting to any instruments.
Patching build_client
The standard approach is to patch build_client at the point where it is called:
# tests/test_wavelength_sweep.py
from unittest.mock import MagicMock, patch
from plesty.scan_exp.measurements import wavelength_sweep
from plesty.lib.device.composite_device import CompositeDevice
def make_mock_device(query_value: str = "1.23e-3") -> MagicMock:
"""Create a mock device that returns a fixed value for any query."""
mock = MagicMock()
mock.query.return_value = query_value
mock.write.return_value = True
mock.identity.return_value = "Mock Device v1.0"
mock.is_operatable = True
return mock
def test_wavelength_sweep_basic():
laser = make_mock_device()
powermeter = make_mock_device(query_value="0.005")
instruments = CompositeDevice({"laser": laser, "powermeter": powermeter})
result = wavelength_sweep(instruments, start_nm=1020, stop_nm=1025, step_nm=1)
assert result.wavelengths == [1020, 1021, 1022, 1023, 1024, 1025]
assert len(result.powers) == 6
assert all(p == 0.005 for p in result.powers)
def test_wavelength_sweep_calls_write_in_order():
laser = make_mock_device()
powermeter = make_mock_device()
instruments = CompositeDevice({"laser": laser, "powermeter": powermeter})
wavelength_sweep(instruments, start_nm=1060, stop_nm=1062, step_nm=1, settle_ms=0)
write_calls = [call[0][1] for call in laser.write.call_args_list]
assert write_calls == [1060, 1061, 1062]
def test_wavelength_sweep_respects_step():
laser = make_mock_device()
powermeter = make_mock_device()
instruments = CompositeDevice({"laser": laser, "powermeter": powermeter})
result = wavelength_sweep(instruments, start_nm=1000, stop_nm=1010, step_nm=5)
assert result.wavelengths == [1000, 1005, 1010]
Why Gate d1 does not apply to experiments
Gate d1 checks that the device's DevicePipeline mock tests are present and passing. Experiments are not devices — they do not inherit BaseDeviceSyncModel and do not have param_schema.json. The compliance standard for experiments is the same quantum standard, but Gate d1 is skipped because module_type != "device".
Coverage requirement (Gate 7)
Gate 7 requires ≥ 80% test coverage. For experiment modules, this means covering:
- Measurement functions with mocked instruments
- Edge cases (empty ranges, step > range, minimum values)
- Error paths (device raises an exception mid-sweep)
def test_wavelength_sweep_handles_device_error():
laser = make_mock_device()
powermeter = MagicMock()
powermeter.query.side_effect = RuntimeError("Connection lost")
instruments = CompositeDevice({"laser": laser, "powermeter": powermeter})
import pytest
with pytest.raises(RuntimeError, match="Connection lost"):
wavelength_sweep(instruments, start_nm=1060, stop_nm=1062)
Running tests
uv run pytest tests/ -v
uv run pytest tests/ --cov=plesty/scan_exp --cov-report=term-missing
The coverage report shows which lines are not covered. Focus on branches and error paths rather than chasing 100%.