用模拟函数替换真实返回值和函数实现的 Pythonic 方式

Posted

技术标签:

【中文标题】用模拟函数替换真实返回值和函数实现的 Pythonic 方式【英文标题】:Pythonic way of replacing real return values and implementation of functions with mock ones 【发布时间】:2014-03-31 07:34:08 【问题描述】:

我的 flask+python 应用通过第三方模块 nativelib 从本机二进制文件中提取 json。

基本上,我的函数看起来像这样

def list_users():
    return nativelib.all_users()

然而,对这个第三方模块的强烈依赖和庞大的原生组件被证明是快速开发的巨大障碍。

我想做的是模拟我的list_users 函数的返回值。 此外,我应该能够通过简单地切换一些布尔值来切换回“真实数据”。 这个布尔值可以是代码中的某个属性或某个命令行参数。

我目前设计的解决方案如下所示:

@mockable('users_list')
def list_users():
    return nativelib.all_users()

我已经将上述mockable 实现为一个类:

class mockable:
    mock = False

    # A dictionary that contains a mapping between the api method
    # ..and the file which contains corresponding the mock json response
    __mock_json_resps = 'users_list':'/var/mock/list_user.json', 'user_by_id': '/var/mock/user_1.json'

    def __init__(self, api_method):
        self.api_method = api_method

    def __call__(self, fn):
        @wraps(fn)
        def wrapper():
            if mock:
                # If mocking is enabled,read mock data from json file 
                mock_resp = __mock_json_resps[self.api_method]
                with open(mock_resp) as json_file:
                    return json.load(json_file)
            else:
                return self.fn()

        return wrapper

在我的入口点模块中,我可以使用

mockable.mock = True

现在虽然这可能有效,但我很想知道这是否是“pythonic”的做法。

如果没有,实现这一目标的最佳方法是什么?

【问题讨论】:

有人建议使用unitest.mock,但我不确定是否要引入对unittest 的依赖。此外,这将如何让我在我的开发环境中快速打开或关闭模拟? 【参考方案1】:

就个人而言,我更喜欢使用mock 库,尽管它是一个额外的依赖项。此外,模拟仅在我的单元测试中使用,因此实际代码不会与测试代码混合。所以,在你的位置,我会保留你的功能:

def list_users():
    return nativelib.all_users()

然后在我的 unittest 中,我会“临时”修补原生库:

def test_list_users(self):
    with mock.patch.object(nativelib, 
                           'all_users', 
                           return_value='json_data_here')):
        result = list_users()
        self.assertEqual(result, 'expected_result')

另外,为了编写多个测试,我通常将模拟代码变成装饰器,例如:

def mock_native_all_users(return_value):
    def _mock_native_all_users(func):
        def __mock_native_all_users(self, *args, **kwargs):
            with mock.patch.object(nativelib, 
                                   'all_users', 
                                   return_value=return_value):
                return func(self, *args, **kwargs)
        return __mock_native_all_users
    return _mock_native_all_users

然后使用它:

@mock_native_all_users('json_data_to_return')
def test_list_users(self):
    result = list_users()
    self.assertEqual(result, 'expected_result')

【讨论】:

您关于仅在单元测试中使用模拟的观点是有道理的。但是,由于这是我构建的一个 Web 应用程序,我什至想模拟,以便我可以编写 angularjs 前端和 python Web 服务器之间的集成测试【参考方案2】:

您在入口代码中设置了mockable.mock,也就是说,您不会在单次运行期间在不同时间动态地打开和关闭它。所以我倾向于认为这是一个依赖配置问题。这不一定更好,只是解决问题的方式略有不同。

如果您不介意检查nativelib.all_users,那么您不需要触摸list_users,只需替换它的依赖项即可。例如,如果 nativelib 仅在一个地方导入,则作为快速单发:

if mock:
    class NativeLibMock:
        def all_users(self):
            return whatever # maybe configure the mock with init params
    nativelib = NativeLibMock()
else:
    import nativelib

如果您对检查感到烦恼,那么额外的self 参数等可能存在问题。实际上,您可能不想复制我上面的代码,或者在依赖nativelib 的不同模块中使用不同的NativeLibMock 实例。如果您要定义一个模块来包含您的模拟对象,那么您最好只实现一个模拟模块。

因此,您当然可以重新实现模块以返回模拟数据,并根据您是否需要模拟调整PYTHONPATH(或sys.path),或者执行import mocklib as nativelib。您选择哪一种取决于您是想模拟nativelib所有 用法还是仅模拟这个。基本上,如果您想模拟 nativelib 的原因是因为它还没有被编写,那么您在快速开发期间会做同样的事情 ;-)

考虑'users_list' 的规范作为list_users 的模拟数据的标记,位于list_users 中是多么重要。如果它有用,那么装饰 list_users 是一个好处,但您仍然可能只想在模拟与非模拟案例中提供不同的装饰器定义。非模拟“装饰器”可以只返回 fn 而不是包装它。

【讨论】:

如果我重新声明我的班级NativeLibMock,是否也需要我重新实现nativelib 中的每个方法?这意味着我将有两个相同方法的完整实现——一个是真实的,另一个是模拟的。对于嘲笑之类的东西来说似乎有点太多了。 我想说的另一点是,因为这是一个烧瓶应用程序,我总是可以通过发送一些参数作为登录请求的一部分来切换模拟

以上是关于用模拟函数替换真实返回值和函数实现的 Pythonic 方式的主要内容,如果未能解决你的问题,请参考以下文章

PHPUnit 如何获得模拟方法的真实返回值?

Python爬虫编程思想(35):用正则表达式搜索替换和分隔字符串

10个常用的损失函数及Python代码实现

python开发函数进阶:匿名函数

python匿名函数

arima.sim() 函数具有不同的:样本大小、phi 值和 sd 值