Is there a way to mock/patch all functions in a module at once (or patch whole module)?

Question:

I have a module that talks to some devices via serial ports. I also have some sort of abstraction of the serial port communication. I want to replace all functions of this module with my fakes.

Example files:

# fancy_module.py

def foo():
    print("foo func from orig")

def bar():
    print("bar func from orig")

def baz():
    print("baz func from orig")

Instead of calling functions from fancy_module.py I want fake functions from fake_module.py to get called during testing or developing mode.

# fake_module.py

def foo():
    print("this is my replacement func of foo")

def bar():
    print("this is my replacement func of bar")

def baz():
    print("this is my replacement func of baz")

I have tried mock and patch from unittest.mock.

I had a little success with patch.

What I have accomplished so far:

# main.py

import fancy_module
import fake_module
from mock import patch

fancy_module.foo()   # calls to orig module
fancy_module.bar()
fancy_module.baz()

# patching each function
with ( patch('fancy_module.foo', new=fake_module.foo),
       patch('fancy_module.bar', new=fake_module.bar),
       patch('fancy_module.baz', new=fake_module.baz) ):

    fancy_module.foo()
    fancy_module.bar()
    fancy_module.baz()

# what I really wan't is patching all functions at once like
with patch_or_replace('fancy_module', new=fake_module):
    fancy_module.foo()

But providing a patch (or mock) function by function is cumbersome. Is there a way to mock or patch all functions in a module at once? The fake module will have the same functions as the module to mock.

Asked By: Thomas Anderson

||

Answers:

The unittest.mock module actually provides a handy tool named patch.multiple for when you want to mock more than one object at the same time. Just like patch.object, it requires you to pass the object to be patched or its qualified name as the first (target) argument, but unlike its sibling it takes arbitrary keyword-arguments, whose names stand for the members of that target to mock, with the corresponding values being the objects to put in their place.

If you are sure that the function names present in the original module are a subset of (or the same as) those in the replacement module, you can write a simple little function using capabilities from the inspect module to construct all those keyword-arguments for you dynamically in a single step. Then you can pass those to patch.multiple and you’ll have the desired effect:

from inspect import getmembers, isfunction
from typing import Any
from unittest.mock import patch

# ... import fancy_module, fake_module


def get_patch_kwargs(orig: object, repl: object) -> dict[str, Any]:
    return {
        name: getattr(repl, name)
        for name, _function in getmembers(orig, isfunction)
    }


def test() -> None:
    # Originals:
    fancy_module.foo()
    fancy_module.bar()
    fancy_module.baz()

    kwargs = get_patch_kwargs(fancy_module, fake_module)
    with patch.multiple(fancy_module, **kwargs):
        # Replacements:
        fancy_module.foo()
        fancy_module.bar()
        fancy_module.baz()


if __name__ == "__main__":
    test()

Output:

foo func from orig
bar func from orig
baz func from orig
this is my replacement func of foo
this is my replacement func of bar
this is my replacement func of baz

A few things to note:

The get_patch_kwargs function will obviously fail with an AttributeError, if it finds a function in orig, but does not find its name in repl. So you need to be sure that the latter has all the names representing functions in the former.

The patcher will not check, if the types of the replacement objects are actually compatible with the original objects. If for example fake_module.foo is (for some reason) not a function but an integer, your test will fail at fancy_module.foo() inside the patch context because obviously integers are not callable.

One nice thing about this setup is that it works exactly the same for other objects, like classes for instance. You could apply the same logic to patch all methods on a class with those from another class. Or replace methods on a class with functions from some module for that matter. As long as the namespaces "match", the function should work and provide you with the correct keyword-arguments for patching and as long as the types are compatible, there should be no problem.

At the same time, this way of patching allows more fine-grained control over what to mock and what to keep as is. Right now, it will replace all user-defined functions, but it will keep intact everything else. And you can easily modify and customize, what specifically to mock or not to mock, by modifying the dictionary. That is why I put it in a separate function to indicate that its behavior can be decoupled from the patcher.

Answered By: Daniil Fajnberg
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.