在 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 标准库...
我有一个从 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 标准库...