How to mock pip package in pytest

Ats
2 min readDec 9, 2024

--

This is a note about what I did to mock pip package in pytest last month

Photo by Claudio Schwarz on Unsplash

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_bis called when the Manageris 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

  1. some.GPIO.library is mocked with mock_gpio_a when the test_led.py is executed
  2. classes.led is cached with mock_gpio_a in sys.modules
  3. sys.modules returns cached classes.led with mock_gpio_a when the test_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!

--

--

Ats
Ats

Written by Ats

I like building something tangible like touch, gesture, and voice. Ruby on Rails / React Native / Yocto / Raspberry Pi / Interaction Design / CIID IDP alumni

No responses yet