pytest-monkeypatch装饰器(不使用模拟/补丁)

时间:2018-07-22 15:27:09

标签: python pytest monkeypatching

我正在使用带有Monkeypatch夹具的pytest编写一些测试。按照规则,我将导入类和方法以从正在使用它们的模块中而不是从源中模拟出来。

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

到目前为止,一切工作都很好,但是在尝试模拟装饰器功能时遇到了问题。

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

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.UserDetails)的文件夹中名为UserDetails.py的文件中的一个基本示例

from decorators import user_login_required

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

在测试模块中,我要像这样设置测试:

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,然后是要替换的方法名称,然后是模拟方法。...

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

我尝试调整代码以查看是否可以通过更改装饰器的导入和使用方式来解决问题:

import decorators

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

然后在测试中,我尝试像这样对函数名称打补丁.....

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)

尽管此代码不会引发任何错误,但是当我逐步进行测试时,该代码从不输入模拟_用户_登录_需要_功能。它总是进入现场装饰器。

我在做什么错?这是尝试尝试猴子装饰器的一个问题,还是无法修补模块中的孤立功能?

3 个答案:

答案 0 :(得分:1)

看来,这里的快速答案只是简单地移动Handler导入,以便它在修补程序之后发生。装饰器和装饰器功能必须位于单独的模块中,以便python在修补装饰器之前不会执行装饰器。

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模块中的补丁功能,您可能会发现更容易实现此目的。

答案 1 :(得分:0)

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

目前,我已经创建了一个夹具来设置环境变量:

@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方法:

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()的调用返回具有访问权限设置的有效对象。我可能会回到这一点。

答案 2 :(得分:0)

我遇到了类似的问题,并通过使用固定装置内的补丁来修补装饰者要延迟的代码来解决此问题。为了提供一些背景信息,我对一个Django项目进行了视图,该项目使用了view函数上的装饰器来强制执行身份验证。有点像:

# myproject/myview.py

@user_authenticated("some_arg")
def my_view():
    ... normal view code ...

user_authenticated的代码位于单独的文件中:

# myproject/auth.py

def user_authenticated(argument):
    ... code for the decorator at some point had a call to:
    actual_auth_logic()
    
    
def actual_auth_logic():
    ... the actual logic around validating auth ...

为了测试,我写了类似的内容:

import pytest
from unittest.mock import patch

@pytest.fixture
def mock_auth():
    patcher = patch("myproject.auth")
    mock_auth = patcher.start()
    mock_auth.actual_auth_logic.return_value = ... a simulated "user is logged in" value
    yield
    patcher.stop()

然后,想要有效跳过身份验证(例如,假设用户已登录)的任何视图测试都可以使用该固定装置:

def test_view(client, mock_auth):
    response = client.get('/some/request/path/to/my/view')

    assert response.content == "what I expect in the response content when user is logged in"

当我想测试说未经身份验证的用户看不到经过身份验证的内容时,我只是省略了身份验证夹具:

def test_view_when_user_is_unauthenticated(client):
    response = client.get('/some/request/path/to/my/view')

    assert response.content == "content when user is not logged in"

这有点脆弱,因为视图的测试现在绑定到auth机制的内部(即,如果actual_auth_logic方法被重命名/重构,那将是糟糕的时光),但至少它是孤立的只是夹具。