在python中模拟整个模块

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如果失败则返回.它似乎是一种公共方法,但它并没有真正记录在案.

  • 此答案中给出了使用“pytest”的类似方法:/sf/answers/3380368911/ (2认同)

jfe*_*ard 6

虽然 @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标准库......