0

I'm working on a python project where I'll be pulling in an external dependency called neopixel.

import time

import board
from rainbowio import colorwheel
import neopixel # here's our module's import


class Lane:
    def __init__(self, pin, total_pixels):
        self.pin = pin
        self.total_pixels = total_pixels
        self.leds = []
        self.initialize_leds()

    def initialize_leds(self, brightness=0.3, auto_write=False):

        # and here's how we use it
        self.leds = neopixel.NeoPixel(self.pin, self.total_pixels, brightness=brightness, auto_write=auto_write)

I'm trying to write out unit tests for this project, so I want to mock out the dependency. Moreover I need to mock out the dependency because when you import it, it imports a couple of other transient dependencies that error out the code because it's expected to run on a microcontroller.

enter image description here

I tried patching the NeoPixel class, but kept running into this error and then realized that even though I'm patching the class, the entire file (including it's imports) is still being imported in an running during the code so those top imports are still being pulled in.

Is there a way of mocking an entire file so that the imports aren't brought in as well?

Chris Schmitz
  • 20,160
  • 30
  • 81
  • 137

1 Answers1

0

The answers to this question are all relevant. Both the accepted answer -- which I am going to demonstrate here -- and this answer concerning dependency injection are good ideas.

Using the accepted answer, we could write a test module test_lane.py like this:

import sys
import types
from unittest import mock

fakes = {}
for name in ['neopixel', 'rainbowio', 'board']:
    fakes[name] = types.ModuleType(name)
    sys.modules[name] = fakes[name]

# This is necessary because of `from rainbowio import colorwheel`
fakes['rainbowio'].colorwheel = types.ModuleType('colorwheel')

import lane


def test_lane():
    with mock.patch('lane.neopixel.NeoPixel', create=True) as fake_np:
        l = lane.Lane(1, 100)
        fake_np.assert_called_with(1, 100, brightness=0.3, auto_write=False)

I can run this on my local system -- with your example Lane class in lane.py -- even though I don't have any of neopixel, rainbowio, or board available.


Another option that I've used in the past when developing for a microcontroller is to simply use stub modules locally. That is, rather than installing the neopixel module on my development system, I would have in the same directory as lane.py a file neopixel.py like this:

class NeoPixel:
  def __init__(pin, pixels, brightness=0, auto_write=False):
    pass

I would populate that with whatever additional methods are used in my code.

This is sufficient to keep syntax checkers happy and provides a point to attach mock objects.

larsks
  • 277,717
  • 41
  • 399
  • 399
  • This answer is fantastic! I had seen the other stackoverflow answers and kind of understood, but this made it click for me. Thanks for taking the time to write it out :bows: – Chris Schmitz Jul 09 '23 at 11:34