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

huangapple go评论65阅读模式
英文:

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

问题

我有一个模块,通过串口与一些设备通信。我还有一种串口通信的抽象。我想用我的伪装函数替换此模块的所有函数。

示例文件:

# fancy_module.py

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

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

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

我希望在测试或开发模式中,不调用来自fancy_module.py的函数,而是调用来自fake_module.py的伪装函数。

# 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")

我已经尝试了unittest.mock中的mockpatch

我使用patch取得了一些小成功。

到目前为止,我已经完成的工作有:

# main.py

import fancy_module
import fake_module
from mock import patch

fancy_module.foo()   # 调用原始模块的函数
fancy_module.bar()
fancy_module.baz()

# 逐个函数进行修补
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()

# 我真正想要的是一次性修补所有函数,就像这样
with patch_or_replace('fancy_module', new=fake_module):
    fancy_module.foo()

但逐个函数提供修补(或模拟)函数很繁琐。是否有一种方法可以一次性模拟或修补模块中的所有函数?伪装模块将具有与要模拟的模块相同的函数。

英文:

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.

答案1

得分: 0

unittest.mock 模块实际上提供了一个方便的工具,名为 patch.multiple,用于当您想要同时模拟多个对象时。与 patch.object 一样,它要求您将要被修补或其限定名称作为第一个(target)参数传递,但与它的兄弟不同,它接受任意的关键字参数,其名称代表要模拟的目标的成员,相应的值是要放在其位置的对象。

如果您确信原始模块中的函数名是替代模块中的子集(或相同的),您可以使用 inspect 模块的功能来动态为您构建所有这些关键字参数,然后将它们传递给 patch.multiple,您将获得所期望的效果:

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:
    # 原始函数:
    fancy_module.foo()
    fancy_module.bar()
    fancy_module.baz()

    kwargs = get_patch_kwargs(fancy_module, fake_module)
    with patch.multiple(fancy_module, **kwargs):
        # 替代函数:
        fancy_module.foo()
        fancy_module.bar()
        fancy_module.baz()

if __name__ == "__main__":
    test()

输出:

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

需要注意的几点:

get_patch_kwargs 函数显然会在在 orig 中找到一个函数,但在 repl 中找不到其名称时引发 AttributeError。因此,您需要确保后者具有前者表示函数的所有名称。

修补程序不会检查替代对象的类型是否实际与原始对象兼容。例如,如果出于某种原因 fake_module.foo 不是函数而是整数,那么在 patch 上下文中,您的测试将在 fancy_module.foo() 处失败,因为显然整数不可调用。

这种设置的一个好处是,它对其他对象(例如类)的操作方式完全相同。您可以使用相同的逻辑来修补类上的所有方法,使用另一个类中的方法来替换类的方法,或者用模块中的函数来替换类的方法。只要命名空间“匹配”,该函数应该运行并为您提供正确的用于修补的关键字参数,只要类型兼容,就不应该有问题。

同时,这种修补的方式允许更精细地控制要模拟和要保留的内容。现在,它将替换所有用户定义的函数,但将保持其他所有内容不变。您可以通过修改字典来轻松修改和自定义要具体模拟或不模拟的内容。这就是为什么我将它放在一个单独的函数中,以指示其行为可以与修补程序分离。

英文:

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:

<pre>
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
</pre>

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.

huangapple
  • 本文由 发表于 2023年2月23日 23:08:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/75546656.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定