This is a note about what I did to mock pip package in pytest last month
Background
I’m working on firmware development with Python and writing unit tests using pytest. Then, I faced cases where the firmware depended on the hardware interface, such as I/O pins, Bus, and so on. When I mocked the codes made by myself, mocking the interfaces was straightforward. However, it wasn't that straightforward when the interfaces were used in pip packages like below. I extracted the related code lines.
# classes/led.py
import some.GPIO.library as GPIO
class LED:
def __init__():
GPIO()
# classes/manager.py
from classes.led import LED
class Manager:
def __init__():
LED()
# test_led.py
@pytest.fixture
def import_led_with_mocked_gpio():
mock_gpio_a = MagicMock()
sys.modules["some.GPIO.library"] = mock_gpio
from classes.led import LEDController
return (mock_gpio_a, LEDController)
# test_manager.py
@pytest.fixture
def import_manager_with_mocked_gpio():
mock_gpio_b = MagicMock()
sys.modules["some.GPIO.library"] = mock_gpio
from classes.manager import Manager
return (mock_gpio_b, Manager)
I tried to assert whether the mock_gpio_b
is called when the Manager
is initialized. But it didn’t work as I expected. So I started to investigate.
What I did
First of all, I checked the import
implementation and found the following code line.
From the code line, Python caches the libraries which has been already imported before in sys.modules
. That’s why I couldn’t mock to import some.GPIO.library
as I expected. What happened to me was
some.GPIO.library
is mocked withmock_gpio_a
when thetest_led.py
is executedclasses.led
is cached withmock_gpio_a
insys.modules
sys.modules
returns cachedclasses.led
withmock_gpio_a
when thetest_manager.py
is executed
Then, eventually, mock_gpio_b.assert_called_once()
didn’t work because the some.GPIO.library
was mocked in classes.led.py
with mock_gpio_a
and cached in sys.modules
. So I created the fixture to clear the cache.
@pytest.fixture
def clear_system_module_cache():
if "classes.leds" in sys.modules:
del sys.modules["classes.leds"]
yield
del sys.modules["classes.leds"]
Then the initial code snippets became below.
# test_led.py
@pytest.fixture
def clear_system_module_cache():
if "classes.leds" in sys.modules:
del sys.modules["classes.led"]
yield
del sys.modules["classes.led"]
@pytest.fixture
def import_led_with_mocked_gpio(clear_system_module_cache):
mock_gpio_a = MagicMock()
sys.modules["some.GPIO.library"] = mock_gpio
from classes.led import LEDController
return (mock_gpio_a, LEDController)
# test_manager.py
@pytest.fixture
def import_manager_with_mocked_gpio():
mock_gpio_b = MagicMock()
sys.modules["some.GPIO.library"] = mock_gpio
from classes.manager import Manager
return (mock_gpio_b, Manager)
After that, the mock_gpio_b
worked as I expected.
That’s it!