Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Unit Testing Tips and Tricks

Dominic Oram edited this page Jul 18, 2023 · 3 revisions

Testing using ophyd.sim devices

PVs which are meant to be linked

For many real devices, such as the Zebra, or EPICS motors, we set a demand value and read back a readback value. ophyd.sim devices don't have a means of connecting PVs to emulate this, but we can do it ourselves. For example (adapted from test_zebra_setup.py - more examples there), we can replace the set method on our demand value with a Mock which ensures our readback value is updated:

from unittest.mock import MagicMock
from ophyd.status import Status

def test_zebra_arm_disarm(
    zebra: Zebra,
):
    def mock_set_armed(val: int):
        zebra.pc.armed.set(val)
        return Status(done=True, success=True)

    mock_arm_disarm = MagicMock(side_effect=mock_set_armed)
    zebra.pc.arm_demand.set = mock_arm_disarm

When something like

yield from bps.abs_set(zebra.pc.arm_demand, 1)

is executed in the RunEngine, this will ensure the value is passed on to the readback value, allowing a plan which relies on this behaviour to be simulated.

Using Fake Motors

If you try and use an ophyd sim motor you will get an error like:

File "/scratch/ffv81422/artemis/python-artemis/.venv/lib/python3.10/site-packages/ophyd/sim.py", line 1472, in check_value
   if self._use_limits and not self.limits[0] <= value <= self.limits[1]:
TypeError: 'NoneType' object is not subscriptable

You can fix this by doing myotor.user_setpoint._use_limits = False on the simulated motor. See https://github.com/bluesky/ophyd/issues/1135 for hopefully fixing this in ophyd

Patching in unit tests

The patch decorator is very useful for being able to test small parts of code but you can miss potential errors. Particularly the test will still pass even if the function is not using the correct parameters. i.e. the code below will fail in production as my_other_func requires a parameter but the test will look like it passes.

def my_other_func(a_parameter):
    ...

def func_under_test():
    my_other_func()

@patch("my_other_func")
def test():
    func_under_test()

This happens because the patch will replace with a MagicMock that will be happy with any parameters. You can get the test to fail (and so be more realistic) by doing:

@patch("my_other_func", autospec=True)
def test():
    func_under_test()

There are some reasons to not always do this, particularly if you're patching objects but in general you should do this as much as possible. For more info, see https://docs.python.org/3.3/library/unittest.mock.html#autospeccing