Can I "fake" a package (or at least a module) in python for testing purposes?

Question:

I want to fake a package in python. I want to define something so that the code can do

from somefakepackage.morefakestuff import somethingfake

And somefakepackage is defined in code and so is everything below it. Is that possible? The reason for doing this is to trick my unittest that I got a package ( or as I said in the title, a module ) in the python path which actually is just something mocked up for this unittest.

Asked By: Weholt

||

Answers:

Sure. Define a class, put the stuff you need inside that, assign the class to sys.modules["classname"].

class fakemodule(object):

    @staticmethod
    def method(a, b):
        return a+b

import sys
sys.modules["package.module"] = fakemodule

You could also use a separate module (call it fakemodule.py):

import fakemodule, sys

sys.modules["package.module"] = fakemodule
Answered By: kindall

You could fake it with a class which behaves like somethingfake:

try:
    from somefakepackage.morefakestuff import somethingfake
except ImportError:
    class somethingfake(object):
         # define what you'd expect of somethingfake, e.g.:
         @staticmethod
         def somefunc():
             ...
         somefield = ...
Answered By: Oben Sonne

Yes, you can make a fake module:

from types import ModuleType
m = ModuleType("fake_module")

import sys
sys.modules[m.__name__] = m

# some scripts may expect a file
# even though this file doesn't exist,
# it may be used by Python for in error messages or introspection.
m.__file__ = m.__name__ + ".py"

# Add a function
def my_function():
    return 10

m.my_function = my_function

Note, in this example its using an actual module (of ModuleType) since some
Python code may expect modules, (instead of a dummy class).

This can be made into a utility function:

def new_module(name, doc=None):
    import sys
    from types import ModuleType
    m = ModuleType(name, doc)
    m.__file__ = name + '.py'
    sys.modules[name] = m
    return m

print(new_module("fake_module", doc="doc string"))

Now other scripts can run:

import fake_module
Answered By: ideasman42

I took some of the ideas from the other answers and turned them into a Python decorator @modulize which converts a function into a module. This module can then be imported as usual. Here is an example.

@modulize('my_module')
def my_dummy_function(__name__):  # the function takes one parameter __name__
    # put module code here
    def my_function(s): 
        print(s, 'bar')

    # the function must return locals()
    return locals()

# import the module as usual
from my_module import my_function
my_function('foo') # foo bar

The code for the decorator is as follows

import sys
from types import ModuleType

class MockModule(ModuleType):
    def __init__(self, module_name, module_doc=None):
        ModuleType.__init__(self, module_name, module_doc)
        if '.' in module_name:
            package, module = module_name.rsplit('.', 1)
            get_mock_module(package).__path__ = []
            setattr(get_mock_module(package), module, self)

    def _initialize_(self, module_code):
        self.__dict__.update(module_code(self.__name__))
        self.__doc__ = module_code.__doc__

def get_mock_module(module_name):
    if module_name not in sys.modules:
        sys.modules[module_name] = MockModule(module_name)
    return sys.modules[module_name]

def modulize(module_name, dependencies=[]):
    for d in dependencies: get_mock_module(d)
    return get_mock_module(module_name)._initialize_

The project can be found here on GitHub. In particular, I created this for programming contests which only allow the contestant to submit a single .py file. This allows one to develop a project with multiple .py files and then combine them into one .py file at the end.

Answered By: Jason Rute

TL;DR

Patch sys.modules using unittest.mock:

mock.patch.dict(
    sys.modules,
    {'somefakepackage': mock.Mock()},
)

Explanation

Other answers correctly recommend to fix sys.modules but a proper way to do it is by patching it using mock.patch. Meaning replacing it temporarily (only for when tests are run) with a fake object that optionally imitates the desired behaviour. And restoring it back once tests are finished to not affect other test cases.

The code in TL;DR section will simply make your missing package not raise ImportError. To provide fake package with contents and imitate desired behaviour, initiate mock.Mock(…) with proper arguments (e.g. add attributes via Mock’s **kwargs).

Full code example

The code below temporarily patches sys.modules so that it includes somefakepackage and makes it importable from the dependent modules without ImportError.

import sys
import unittest
from unittest import mock

class SomeTestCase(unittest.TestCase):
    def test_smth(self):
        # implement your testing logic, for example:
        self.assertEqual(
            123,
            somefakepackage_dependent.some_func(),
        )

    @classmethod
    def setUpClass(cls):  # called once before all the tests
        # define what to patch sys.modules with
        cls._modules_patcher = mock.patch.dict(
            sys.modules,
            {'somefakepackage': mock.Mock()},
        )
        # actually patch it
        cls._modules_patcher.start()
        # make the package globally visible and import it,
        #   just like if you have imported it in a usual way
        #   placing import statement at the top of the file,
        #   but relying on a patched dependency
        global somefakepackage_dependent
        import somefakepackage_dependent

    @classmethod  # called once after all tests
    def tearDownClass(cls):
        # restore initial sys.modules state back
        cls._modules_patcher.stop()

To read more about setUpClass/tearDownClass methods, see unittest docs.

unittest‘s built-in mock subpackage is actually a very powerful tool. Better dive deeper into its documentation to get a better understanding.

Answered By: Anton Bryzgalov
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.