pytest-monkeypatch 装饰器(不使用模拟/补丁)
pytest-monkeypatch a decorator (not using mock / patch)
我正在使用 pytest 和 monkeypatch 夹具编写一些测试。按照规则,我正在导入 classes 和方法,以从它们正在使用的模块中模拟出来,而不是从源代码中模拟出来。
我正在为其编写测试的应用程序是一个 Google App Engine 应用程序,它使用标准环境。因此我必须使用 python 2.7,我使用的实际版本是 2.7.15 - pytest 版本是 3.5.0
到目前为止一切都运行良好,但我在尝试模拟装饰器函数时遇到了问题。
从头开始。在一个名为 decorators.py 的 py 文件中包含所有 auth 装饰器,包括我想要模拟的装饰器。有问题的装饰器是一个模块函数,而不是 class.
的一部分
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 的文件中的基本示例
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)
虽然这段代码没有抛出任何错误,但当我单步执行测试时,代码从未进入 mock_user_login_required_func。它总是进入实时装饰器。
我做错了什么?这是一般尝试对装饰器进行 monkeypatch 的问题,或者模块中的单独函数是否可以不被修补?
看起来这里的快速答案只是简单地移动您的处理程序导入,以便它发生在补丁之后。装饰器和装饰函数必须在不同的模块中,这样 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 模块的补丁功能更容易完成此操作。
由于这里提到的导入/修改陷阱,我决定避免尝试对这个特定的装饰器使用模拟。
目前我已经创建了一个固定装置来设置环境变量:
@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,然后在测试请求的 headers 中发送它。这将使对 self.auth.get_user_by_session() return 的调用成为具有访问集的有效 object。我可能会回到这个。
我有一个类似的问题,并通过在固定装置中使用补丁来修补装饰器延迟的代码来解决它。为了提供一些上下文,我有一个 Django 项目的视图,该项目在视图函数上使用装饰器来强制执行身份验证。有点像:
# 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"
当我想测试说未经身份验证的用户看不到经过身份验证的内容时,我只是省略了 auth fixture:
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
方法是 renamed/refactored 那就不好了),但是至少它只与固定装置隔离。
我正在使用 pytest 和 monkeypatch 夹具编写一些测试。按照规则,我正在导入 classes 和方法,以从它们正在使用的模块中模拟出来,而不是从源代码中模拟出来。
我正在为其编写测试的应用程序是一个 Google App Engine 应用程序,它使用标准环境。因此我必须使用 python 2.7,我使用的实际版本是 2.7.15 - pytest 版本是 3.5.0
到目前为止一切都运行良好,但我在尝试模拟装饰器函数时遇到了问题。
从头开始。在一个名为 decorators.py 的 py 文件中包含所有 auth 装饰器,包括我想要模拟的装饰器。有问题的装饰器是一个模块函数,而不是 class.
的一部分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 的文件中的基本示例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)
虽然这段代码没有抛出任何错误,但当我单步执行测试时,代码从未进入 mock_user_login_required_func。它总是进入实时装饰器。
我做错了什么?这是一般尝试对装饰器进行 monkeypatch 的问题,或者模块中的单独函数是否可以不被修补?
看起来这里的快速答案只是简单地移动您的处理程序导入,以便它发生在补丁之后。装饰器和装饰函数必须在不同的模块中,这样 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 模块的补丁功能更容易完成此操作。
由于这里提到的导入/修改陷阱,我决定避免尝试对这个特定的装饰器使用模拟。
目前我已经创建了一个固定装置来设置环境变量:
@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,然后在测试请求的 headers 中发送它。这将使对 self.auth.get_user_by_session() return 的调用成为具有访问集的有效 object。我可能会回到这个。
我有一个类似的问题,并通过在固定装置中使用补丁来修补装饰器延迟的代码来解决它。为了提供一些上下文,我有一个 Django 项目的视图,该项目在视图函数上使用装饰器来强制执行身份验证。有点像:
# 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"
当我想测试说未经身份验证的用户看不到经过身份验证的内容时,我只是省略了 auth fixture:
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
方法是 renamed/refactored 那就不好了),但是至少它只与固定装置隔离。