Tzu*_*rEl 8 python unit-testing python-module mocking python-mock
我有一个从PyPI导入模块的应用程序.我想为该应用程序的源代码编写单元测试,但我不想在这些测试中使用PyPI中的模块.
我想完全模拟它(测试机器不会包含那个PyPI模块,所以任何导入都会失败).
目前,每次我尝试加载我想在单元测试中测试的类时,我都会立即收到导入错误.所以我想也许用
try:
except ImportError:
Run Code Online (Sandbox Code Playgroud)
并捕获该导入错误,然后使用command_module.run().这看起来非常危险/丑陋,我想知道是否还有另一种方式.
另一个想法是编写一个适配器来包装PyPI模块,但我仍在努力.
如果你知道我可以模拟整个python包,我会非常感激.谢谢.
Don*_*kby 10
如果您想深入了解Python导入系统,我强烈推荐David Beazley的演讲.
至于您的具体问题,这是一个在缺少依赖关系时测试模块的示例.
bar.py
- 缺少my_bogus_module时要测试的模块
from my_bogus_module import foo
def bar(x):
return foo(x) + 1
Run Code Online (Sandbox Code Playgroud)
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')
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)
您应该通过检查my_bogus_module
运行测试时实际不可用的内容来使安全性更高一些.您还可以查看pydoc.locate()
将尝试导入某些内容的方法,None
如果失败则返回.它似乎是一种公共方法,但它并没有真正记录在案.
虽然 @Don Kirkby 的答案是正确的,但您可能想看看更大的图景。我借用了已接受答案中的示例:
import pypilib
def bar(x):
return pypilib.foo(x) + 1
Run Code Online (Sandbox Code Playgroud)
由于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
Run Code Online (Sandbox Code Playgroud)
现在,您可以对该函数进行单元测试:
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()
Run Code Online (Sandbox Code Playgroud)
您可能会不同意该设计。try
/部分except
有点麻烦,特别是当您pypilib
在应用程序的多个模块中使用该模块时。并且您必须为每个依赖外部库的函数添加一个参数。
但是,将依赖项注入到外部库的想法很有用,因为即使外部库不在您的控制范围内,您也可以控制类方法的输入并测试输出。特别是如果导入的模块是有状态的,则该状态可能很难在单元测试中重现。在这种情况下,将模块作为参数传递可能是一个解决方案。
但处理这种情况的常用方法称为依赖倒置原则( 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
Run Code Online (Sandbox Code Playgroud)
每次您需要这些函数之一时,只需在代码中注入该类的一个实例即可:
class Application:
def __init__(self, util: MyUtil):
self._util = util
def something(self, x, y):
return self._util.baz(self._util.bar(x), y)
Run Code Online (Sandbox Code Playgroud)
该类MyUtil
必须尽可能精简,但必须保持对底层库的抽象。这是一个权衡。显然,Application
可以进行单元测试(只需注入 aMock
而不是 的实例MyUtil
),而在某些情况下(例如测试期间不可用的 PyPi 库、仅在框架内运行的模块等),MyUtil
只能在集成测试。如果您需要对应用程序的边界进行单元测试,可以使用@Don Kirkby 的方法。
请注意,单元测试之后的第二个好处是,如果您更改正在使用的库(弃用、许可证问题、成本等),您只需重写该类MyUtil
,使用一些其他库或从头开始编码。您的应用程序受到保护,免受外部世界的影响。
罗伯特·C·马丁 (Robert C. Martin) 的《清洁代码》用了一章完整的章节介绍了边界。
摘要在使用 @Don Kirkby 的方法或任何其他方法之前,请务必定义应用程序的边界,无论您使用的特定库如何。当然,这不适用于Python标准库......