在 python 中模拟整个模块

Mock an entire module in python

我有一个从 PyPI 导入模块的应用程序。 我想为该应用程序的源代码编写单元测试,但我不想在这些测试中使用 PyPI 中的模块。
我想完全模拟它(测试机器不会包含那个 PyPI 模块,所以任何导入都会失败)。

目前,每次我尝试加载 class 我想在单元测试中测试时,我都会立即收到导入错误。所以我想也许使用

try: 
    except ImportError:

并捕获该导入错误,然后使用 command_module.run()。 这看起来很漂亮 risky/ugly,我想知道是否还有其他方法。

另一个想法是编写一个适配器来包装 PyPI 模块,但我仍在努力。

如果您知道我可以模拟整个 python 包的任何方法,我将不胜感激。 谢谢

如果您想深入了解 Python 导入系统,我强烈推荐 David Beazley's talk

至于您的具体问题,这里有一个在缺少依赖项时测试模块的示例。

bar.py - my_bogus_module 缺失时要测试的模块

from my_bogus_module import foo

def bar(x):
    return foo(x) + 1

mock_bogus.py - 包含您的测试的文件将加载模拟模块

from mock import Mock
import sys
import types

module_name = 'my_bogus_module'
bogus_module = types.ModuleType(module_name)
sys.modules[module_name] = bogus_module
bogus_module.foo = Mock(name=module_name+'.foo')

test_bar.py - 在 my_bogus_module 不可用时测试 bar.py

import unittest

from mock_bogus import bogus_module  # must import before bar module
from bar import bar

class TestBar(unittest.TestCase):
    def test_bar(self):
        bogus_module.foo.return_value = 99
        x = bar(42)

        self.assertEqual(100, x)

您可能应该通过在 运行 测试时检查 my_bogus_module 实际上不可用来使它更安全一些。您还可以查看将尝试导入某些内容的 pydoc.locate() 方法,以及 return None 如果失败。这似乎是一种 public 方法,但并没有真正记录下来。

虽然@Don Kirkby 的回答是正确的,但您可能需要纵观全局。我从接受的答案中借用了这个例子:

import pypilib

def bar(x):
    return pypilib.foo(x) + 1

由于 pypilib 仅在生产中可用,因此当您尝试 单元测试 bar 时遇到一些麻烦也就不足为奇了。该函数需要外部库运行,因此必须使用该库进行测试。您需要的是 集成测试 .

也就是说,您可能想要强制进行单元测试,这通常是个好主意,因为它会提高您(和其他人)对代码质量的信心。要扩大单元测试区域,您必须注入依赖项。没有什么能阻止您(在 Python!)将模块作为参数(类型为 types.ModuleType)传递:

try:
    import pypilib     # production
except ImportError:
    pypilib = object() # testing

def bar(x, external_lib = pypilib):
    return external_lib.foo(x) + 1

现在,您可以对函数进行单元测试:

import unittest
from unittest.mock import Mock

class Test(unittest.TestCase):
    def test_bar(self):
        external_lib = Mock(foo = lambda x: 3*x)
        self.assertEqual(10, bar(3, external_lib))
     

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

您可能不赞成该设计。 try/except 部分有点麻烦,尤其是当您在应用程序的多个模块中使用 pypilib 模块时。而且你必须为每个依赖外部库的函数添加一个参数。

但是,向外部库注入依赖项的想法很有用,因为您可以控制输入并测试 class 方法的输出,即使外部库不在您的控制范围内。特别是如果导入的模块是 stateful,则状态可能难以在单元测试中重现。在这种情况下,将模块作为参数传递可能是一种解决方案。

但处理这种情况的通常方法称为dependency inversion principle (the D of SOLID):您应该定义应用程序的(抽象)边界,即您需要从外部世界获得什么。在这里,这是bar和其他功能,最好分组在一个或多个class中:

import pypilib
import other_pypilib

class MyUtil:
    """
    All I need from outside world
    """
    @staticmethod
    def bar(x):
        return pypilib.foo(x) + 1

    @staticmethod
    def baz(x, y):
        return other_pypilib.foo(x, y) * 10.0

    ...
    # not every method has to be static

每次您需要这些功能之一时,只需在您的代码中注入 class 的一个实例:

class Application:
    def __init__(self, util: MyUtil):
        self._util = util
        
    def something(self, x, y):
        return self._util.baz(self._util.bar(x), y)

MyUtil class 必须尽可能精简,但必须对底层库保持抽象。这是一个权衡。显然,Application 可以进行单元测试(只需注入 Mock 而不是 MyUtil 的实例),而在某些情况下(例如测试期间不可用的 PyPi 库,模块 运行s 仅在框架内,等等),MyUtil 只能在集成测试中进行测试。如果您需要对应用程序的边界进行单元测试,可以使用@Don Kirkby 的方法。

请注意,在单元测试之后的第二个好处是,如果您更改正在使用的库(弃用、许可证问题、成本等),您只需重写 MyUtil class,使用其他一些库或从头开始编码。您的应用程序受到保护,不受外界的影响。

Clean Code Robert C. Martin 有一整章关于边界的内容。

总结 在使用@Don Kirkby 的方法或任何其他方法之前,请务必定义您的应用程序的边界,而不管您使用的具体库是什么。当然,这不适用于 Python 标准库...