关于python:pytest-monkeypatch一个装饰器(不使用mock / patch)

pytest-monkeypatch a decorator (not using mock / patch)

我正在使用pytest和monkeypatch fixture编写一些测试。遵循规则我将导入类和方法,以模拟它们正在使用的模块,而不是源。

我正在编写测试的应用程序是一个使用标准环境的Google App Engine应用程序。因此我必须使用python 2.7,我使用的实际版本是2.7.15 - pytest版本是3.5.0

到目前为止,一切都运行良好,但在尝试模拟装饰器功能时遇到了问题。

从顶部开始。在一个名为decorators.py的py文件中包含了所有的auth装饰器,包括我想要模拟的装饰器。有问题的装饰器是一个模块函数,不是类的一部分。

1
2
3
4
5
6
7
8
9
def user_login_required(handler):
    def is_authenticated(self, *args, **kwargs):
        u = self.auth.get_user_by_session()
        if u.access == '' or u.access is None:
            # return the response
            self.redirect('/admin', permanent=True)
        else:
            return handler(self, *args, **kwargs)
    return is_authenticated

装饰器应用于Web请求功能。名为Handlers(handlers.UserDetails)的文件夹中名为UserDetails.py的文件中的基本示例

1
2
3
4
5
6
from decorators import user_login_required

class UserDetailsHandler(BaseHandler):
    @user_login_required
    def get(self):
        # Do web stuff, return html, etc

在测试模块中,我正在设置测试,如下所示:

1
2
3
4
5
6
from handlers.UserDetails import user_login_required

@pytest.mark.parametrize('params', get_params, ids=get_ids)
def test_post(self, params, monkeypatch):

    monkeypatch.setattr(user_login_required, mock_user_login_required_func)

这个问题是monkeypatch不允许我将单个函数作为目标。它希望目标是一个Class,然后是要替换的方法名称,然后是mock方法....

1
monkeypatch.setattr(WouldBeClass,"user_login_required", mock_user_login_required_func)

我试图通过改变装饰器的导入和使用方式来调整代码以查看是否可以绕过它:

1
2
3
4
5
6
import decorators

class UserDetailsHandler(BaseHandler):
    @decorators.user_login_required
    def get(self):
        # Do web stuff, return html, etc

然后在测试中我试着像这样修补函数名.....

1
2
3
4
5
6
from handlers.UserDetails import decorators

@pytest.mark.parametrize('params', get_params, ids=get_ids)
def test_post(self, params, monkeypatch):

    monkeypatch.setattr(decorators,"user_login_required" , mock_user_login_required_func)

虽然此代码不会引发任何错误,但当我单步执行测试时,代码永远不会进入mock_user_login_required_func。它总是进入现场装饰。

我究竟做错了什么?这是一个问题,一般尝试monkeypatch装饰器或可以单独模块中的功能不修补?


看起来这里的快速答案只是移动您的Handler导入,以便在补丁之后发生。装饰器和装饰函数必须位于单独的模块中,以便python在修补之前不会执行装饰器。

1
2
3
4
5
6
7
from decorators import user_login_required

@pytest.mark.parametrize('params', get_params, ids=get_ids)
def test_post(self, params, monkeypatch):

    monkeypatch.setattr(decorators,"user_login_required" , mock_user_login_required_func)
    from handlers.UserDetails import UserDetailsHandler

您可以使用内置的unittest.mock模块中的patch函数更容易地完成此操作。


由于这里提到的导入/修改陷阱,我决定避免尝试使用模拟这个特定的装饰器。

目前我已经创建了一个用于设置环境变量的fixture:

1
2
3
4
5
6
7
@pytest.fixture()
def enable_fake_auth():
   """ Sets the"enable_fake_auth"  then deletes after use"""
    import os
    os.environ["enable_fake_auth"] ="true"
    yield
    del os.environ["enable_fake_auth"]

然后在装饰器中我修改了is_authenticated方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def is_authenticated(self, *args, **kwargs):
    import os
    env = os.getenv('enable_fake_auth')
    if env:
        return handler(self, *args, **kwargs)
    else:
        # get user from session
        u = self.auth.get_user_by_session()
        if u:
            access = u.get("access", None)
            if access == '' or access is None:
                # return the response
                self.redirect('/admin', permanent=True)
            else:
                return handler(self, *args, **kwargs)
        else:
            self.redirect('/admin?returnPath=' + self.request.path, permanent=True)

return is_authenticated

它没有回答我原来提出的问题,但我已将我的解决方案放在这里,以防它可以帮助其他人。正如hoefling指出的那样,修改这样的生产代码通常是一个坏主意,所以使用风险自负!

我之前的原始解决方案没有修改或模拟任何代码。它涉及创建一个虚假的安全cookie,然后在测试请求的标题中发送它。这将调用self.auth.get_user_by_session()返回具有访问集的有效对象。我可以回到这个。