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

时间:2014-02-27 17:26:59

标签: python python-3.x python-decorators

我的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”的做法。

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

2 个答案:

答案 0 :(得分: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参数可能存在问题,依此类推。在实践中,您可能不希望复制我上面的代码,或者让NativeLibMock的不同实例在依赖nativelib的不同模块中徘徊。如果你要定义一个模块来包含你的模拟对象,那么你也可以只实现一个模拟模块。

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

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

答案 1 :(得分:2)

就个人而言,我更喜欢使用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')