Search code examples
pythonunit-testingmockingpython-unittestpython-mock

mocking all classes from a module in python


I have a test_a.py, a.py and b.py in 3 different directories in my test environment.

b.py

class SBD():

def __init__(self):
    print("SBD created (In B)")

a.py

import b
from b import *

print("In module A")

def fun1():
  a=SBD()
  print("SBD created(In A)")

test_a.py

import unittest
import sys
from unittest.mock import Mock,patch,MagicMock

sys.path.append("../abc/")
import b as c
sys.modules['b'] = MagicMock(spec=c)

sys.path.append("../xyz/")
import a

class TestStringMethods(unittest.TestCase):

    def test_isupper(self):
        a.fun1()


if __name__ == '__main__':
    unittest.main()

In a real situation, b.py will have multiple classes and I wanted to mock all of them, so I tried mocking the module b with the same specifications. But when i run the test_a.py it gives me an error saying "SBD" is not defined. What am I doing wrong here?


Solution

  • A MagicMock instance does not provide the same information to the import machinery as a module would. Even with a spec, there is no actual SDB attribute defined on the mock, so from b import * won't find it.

    The from * import machinery tries two different things:

    • It tries to access the name __all__; if defined it must be a list of strings of names to import.
    • If __all__ is not defined, the keys of the __dict__ attribute are taken, filtering out names that start with an underscore.

    Because your b module has no __all__ list defined, the __dict__ keys are taken instead. For a MagicMock instance specced against a module, the __dict__ attribute only consists of names with _ underscores and the mock_calls attribute. from b import * only imports mock_calls:

    >>> import a as c
    >>> module_mock = MagicMock(spec=c)
    >>> [n for n in module_mock.__dict__ if n[:1] != '_']
    ['method_calls']
    

    I would strongly advice against mocking the whole module; doing this would require that you postpone importing a, and is fragile. The patch is permanent (is not undone automatically when tests end) and won't support repeated runs of the test or running tests in random order.

    But if you had to make this work, you could add a __all__ attribute to the mock first:

    sys.modules['b'] = MagicMock(spec=c, __all__=[n for n in c.__dict__ if n[:1] != '_'])
    

    Personally, I'd a) avoid using from module import * syntax altogether. If I could not prevent this from being used anyway, the next step would be to apply patches to a after importing, looping over the b module to obtain specced replacements:

    # avoid manipulating sys.path if at all possible. Move that to a PYTHONPATH
    # variable or install the modules properly.
    import unittest
    from unittest import mock
    
    import a
    import b
    
    class TestStringMethods(unittest.TestCase):
        def setUp(self):
            # mock everything `from b import *` would import
            b_names = getattr(b, '__all__', None)
            if b_names is None:
                b_names = [n for n in b.__dict__ if n[:1] != '_']
            self.b_mocks = {}
            for name in b_names:
                orig = getattr(b, name, None)
                if orig is None:
                    continue
                self.b_mocks[name] = mock.patch.object(a, name, spec=orig)
                self.b_mocks[name].start()
                self.addCleanup(self.b_mocks[name].stop)
    
        def test_isupper(self):
            a.fun1()
    

    This leaves sys.modules['b'] untouched, and processes the exact same names that from * would load. The patches are removed again after the test ends.

    The above test outputs:

    $ python test_a.py
    In module A
    SBD created(In A)
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    OK