-
Notifications
You must be signed in to change notification settings - Fork 5
Unit Testing Tips and Tricks
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.
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
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