在python中模拟整个模块

时间:2016-12-19 10:43:37

标签: python unit-testing mocking python-module python-mock

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

目前,每次我尝试加载我想在单元测试中测试的类时,我都会立即收到导入错误。所以我想到了可能正在使用

try: 
    except ImportError:

并捕获导入错误,然后使用command_module.run()。 这看起来非常危险/丑陋,我想知道是否还有另一种方式。

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

如果你知道我可以模拟整个python包,我会非常感激。 感谢。

2 个答案:

答案 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 principleSOLID的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类其他一些库或从头开始编写代码。您的应用程序不受野外环境的保护。

罗伯特·C(Robert C. Martin)的

清洁代码。关于边界,有一整章内容。

摘要在使用@Don Kirkby的方法或任何其他方法之前,请确保定义应用程序的边界,而与所使用的特定库无关。当然,这不适用于Python标准库...