我有一个从PyPI导入模块的应用程序。
我想为该应用程序的源代码编写单元测试,但我不想在这些测试中使用PyPI中的模块。
我想完全模拟它(测试机器不包含那个PyPI模块,因此任何导入都会失败)。
目前,每次我尝试加载我想在单元测试中测试的类时,我都会立即收到导入错误。所以我想到了可能正在使用
try:
except ImportError:
并捕获导入错误,然后使用command_module.run()。 这看起来非常危险/丑陋,我想知道是否还有另一种方式。
另一个想法是编写一个适配器来包装PyPI模块,但我仍在努力。
如果你知道我可以模拟整个python包,我会非常感激。 感谢。
答案 0 :(得分:8)
如果您想深入了解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
- 在bar.py
不可用时测试my_bogus_module
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()
方法,如果失败则返回None
。它似乎是一种公共方法,但它并没有真正记录下来。
答案 1 :(得分:0)
虽然@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
模块的情况下。并且必须向依赖于外部库的每个函数添加一个参数。
但是,将依赖项注入外部库的想法很有用,因为即使外部库不在控件内,您也可以控制类方法的输入并测试其输出。尤其是如果导入的模块是有状态的,则状态可能难以在单元测试中重现。在这种情况下,将模块作为参数传递可能是解决方案。
但是处理这种情况的通常方法称为dependency inversion principle(SOLID的D):您应该定义应用程序的(抽象)边界,即您需要外界提供的东西。在这里,这是bar
和其他函数,最好分为一个或多个类:
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 Application:
def __init__(self, util: MyUtil):
self._util = util
def something(self, x, y):
return self._util.baz(self._util.bar(x), y)
MyUtil
类必须尽可能小,但必须与基础库保持抽象。这是一个权衡。显然,Application
可以进行单元测试(只需注入Mock
而不是MyUtil
的实例),而在某些情况下(例如在测试过程中不可用的PyPi库),可以运行一个模块MyUtil
仅可以在集成测试中进行测试。如果需要对应用程序的边界进行单元测试,则可以使用@Don Kirkby的方法。
请注意,在单元测试之后,第二个好处是,如果您更改了正在使用的库(弃用,许可问题,成本等),则只需使用以下方法重写MyUtil
类其他一些库或从头开始编写代码。您的应用程序不受野外环境的保护。
清洁代码。关于边界,有一整章内容。
摘要在使用@Don Kirkby的方法或任何其他方法之前,请确保定义应用程序的边界,而与所使用的特定库无关。当然,这不适用于Python标准库...