pytest 核心功能-monkeypatch/mock 模块和环境
有时测试需要调用依赖于全局设置的功能或调用不容易测试的代码,例如网络访问。 monkeypatch fixture
可帮助您安全地设置/删除属性、字典项或环境变量,或修改 sys.path
以进行导入。
monkeypatch fixture
提供了以下帮助方法,用于在测试中安全地打补丁和模拟功能:
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.setattr("somemodule.obj.name", value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=None)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
在请求的测试功能或fixture
完成后,所有修改都将被撤消。raise
参数确定如果设置/删除操作的目标不存在,是否会引发 KeyError
或 AttributeError
考虑以下场景:
- 修改函数的行为或类的属性,例如,有一个API调用或数据库连接,你不会进行测试,但你知道预期的输出应该是什么。使用monkeypatch。将函数或属性与您想要的测试行为进行修补。这可以包括您自己的函数。使用monkeypatch.delattr删除测试的函数或属性。
- 修改字典的值,例如 您有一个要针对某些测试用例修改的全局配置。 使用 monkeypatch.setitem 修补字典以进行测试。 monkeypatch.delitem 可用于删除项目。
- 修改测试的环境变量,例如 如果缺少环境变量,则测试程序行为,或将多个值设置为已知变量。 monkeypatch.setenv 和 monkeypatch.delenv 可用于这些补丁。
- 使用
monkeypatch.setenv("PATH", value, prepend=os.pathsep)
修改 $PATH
,并使用 monkeypatch.chdir
在测试期间更改当前工作目录的上下文。 - 使用
monkeypatch.syspath_prepend
修改 sys.path
,它还将调用 pkg_resources.fixup_namespace_packages
和 importlib.invalidate_caches()
简单示例:monkeypatching 函数
考虑一个使用用户目录的场景。 在测试的上下文中,您不希望您的测试依赖于正在运行的用户。 monkeypatch
可用于修补依赖于用户的函数以始终返回特定值。
在此示例中,monkeypatch.setattr
用于修补 Path.home
,以便在运行测试时始终使用已知的测试路径 Path("/abc")
。 这消除了出于测试目的对运行用户的任何依赖。 必须在调用将使用修补函数的函数之前调用 monkeypatch.setattr
。 测试功能完成后 Path.home
修改将被撤消。
# contents of test_module.py with source code and the test
from pathlib import Path
def getssh():
"""Simple function to return expanded homedir ssh path."""
return Path.home() / ".ssh"
def test_getssh(monkeypatch):
# mocked return function to replace Path.home
# always return '/abc'
def mockreturn():
return Path("/abc")
# Application of the monkeypatch to replace Path.home
# with the behavior of mockreturn defined above.
monkeypatch.setattr(Path, "home", mockreturn)
# Calling getssh() will use mockreturn in place of Path.home
# for this test with the monkeypatch.
x = getssh()
assert x == Path("/abc/.ssh")
Monkeypatching 返回的对象:building mock classes
monkeypatch.setattr
可以与类结合使用,以模拟从函数返回的对象而不是值。 想象一个简单的函数来获取 API url 并返回 json 响应。
# contents of app.py, a simple API retrieval example
import requests
def get_json(url):
"""Takes a URL, and returns the JSON."""
r = requests.get(url)
return r.json()
我们需要mock r
,返回的响应对象用于测试目的。 r
的mock
需要一个返回字典的 .json()
方法。 这可以在我们的测试文件中通过定义一个代表 r
的类来完成。
# contents of test_app.py, a simple test for our API retrieval
# import requests for the purposes of monkeypatching
import requests
# our app.py that includes the get_json() function
# this is the previous code block example
import app
# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:
# mock json() method always returns a specific testing dictionary
@staticmethod
def json():
return {"mock_key": "mock_response"}
def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
return MockResponse()
# apply the monkeypatch for requests.get to mock_get
monkeypatch.setattr(requests, "get", mock_get)
# app.get_json, which contains requests.get, uses the monkeypatch
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
monkeypatch
使用我们的 mock_get
函数对 requests.get
应用模拟。 mock_get
函数返回一个 MockResponse
类的实例,它定义了一个 json()
方法来返回一个已知的测试字典,并且不需要任何外部 API 连接。
您可以为您正在测试的场景构建具有适当复杂程度的 MockResponse
类。 例如,它可以包含一个始终返回 True
的 ok
属性,或者根据输入字符串从 json()
模拟方法返回不同的值。
这个模拟可以使用fixture
在测试之间共享:
# contents of test_app.py, a simple test for our API retrieval
import pytest
import requests
# app.py that includes the get_json() function
import app
# custom class to be the mock return value of requests.get()
class MockResponse:
@staticmethod
def json():
return {"mock_key": "mock_response"}
# monkeypatched requests.get moved to a fixture
@pytest.fixture
def mock_response(monkeypatch):
"""Requests.get() mocked to return {'mock_key':'mock_response'}."""
def mock_get(*args, **kwargs):
return MockResponse()
monkeypatch.setattr(requests, "get", mock_get)
# notice our test uses the custom fixture instead of monkeypatch directly
def test_get_json(mock_response):
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
此外,如果mock
被设计为应用于所有测试,则可以将fixture
移动到conftest.py
文件并使用autuse =True
选项。
全局补丁示例:防止远程操作的请求
如果你想阻止requests
库在所有测试中执行http请求,你可以这样做:
# contents of conftest.py
import pytest
@pytest.fixture(autouse=True)
def no_requests(monkeypatch):
"""Remove requests.sessions.Session.request for all tests."""
monkeypatch.delattr("requests.sessions.Session.request")
将为每个测试函数执行此 autouse fixture
,并将删除方法 request.session.Session.request
以便测试中创建 http 请求的任何尝试都将失败。
请注意,不建议修补内置函数,例如 open
、compile
等,因为它可能会破坏 pytest 的内部结构。 如果这是不可避免的,传递 --tb=native
、--assert=plain
和 --capture=no
可能会有所帮助,尽管不能保证。
请注意,pytest使用的stdlib
函数和一些第三方库补丁可能会破坏pytest本身,因此在这些情况下,建议使用MonkeyPatch.context()
来限制补丁到你想要测试的块:
import functools
def test_partial(monkeypatch):
with monkeypatch.context() as m:
m.setattr(functools, "partial", 3)
assert functools.partial == 3
Monkeypatching环境变量
如果您正在使用环境变量,那么为了测试的目的,您经常需要安全地更改这些值或从系统中删除它们。Monkeypatch
提供了一种使用setenv
和delenv
方法来实现这一点的机制。例如:
# contents of our original code file e.g. code.py
import os
def get_os_user_lower():
"""Simple retrieval function.
Returns lowercase USER or raises OSError."""
username = os.getenv("USER")
if username is None:
raise OSError("USER environment is not set.")
return username.lower()
有两种可能的路径。 首先,将 USER
环境变量设置为一个值。 其次,USER
环境变量不存在。 使用 monkeypatch
可以安全地测试两个路径,而不会影响运行环境:
# contents of our test file e.g. test_code.py
import pytest
def test_upper_to_lower(monkeypatch):
"""Set the USER env var to assert the behavior."""
monkeypatch.setenv("USER", "TestingUser")
assert get_os_user_lower() == "testinguser"
def test_raise_exception(monkeypatch):
"""Remove the USER env var and assert OSError is raised."""
monkeypatch.delenv("USER", raising=False)
with pytest.raises(OSError):
_ = get_os_user_lower()
这种行为可以转移到fixture
结构中,并在测试中共享:
# contents of our test file e.g. test_code.py
import pytest
@pytest.fixture
def mock_env_user(monkeypatch):
monkeypatch.setenv("USER", "TestingUser")
@pytest.fixture
def mock_env_missing(monkeypatch):
monkeypatch.delenv("USER", raising=False)
# notice the tests reference the fixtures for mocks
def test_upper_to_lower(mock_env_user):
assert get_os_user_lower() == "testinguser"
def test_raise_exception(mock_env_missing):
with pytest.raises(OSError):
_ = get_os_user_lower()
Monkeypatching字典
monkeypatch.setitem
可用于在测试期间将字典的值安全地设置为特定值。 以这个简化的连接字符串为例:
# contents of app.py to generate a simple connection string
DEFAULT_CONFIG = {"user": "user1", "database": "db1"}
def create_connection_string(config=None):
"""Creates a connection string from input or defaults."""
config = config or DEFAULT_CONFIG
return f"User Id={config['user']}; Location={config['database']};"
出于测试目的,我们可以将 DEFAULT_CONFIG
字典修补为特定值。
# contents of test_app.py
# app.py with the connection string function (prior code block)
import app
def test_connection(monkeypatch):
# Patch the values of DEFAULT_CONFIG to specific
# testing values only for this test.
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
# expected result based on the mocks
expected = "User Id=test_user; Location=test_db;"
# the test uses the monkeypatched dictionary settings
result = app.create_connection_string()
assert result == expected
您可以使用 monkeypatch.delitem
删除值
# contents of test_app.py
import pytest
# app.py with the connection string function
import app
def test_missing_user(monkeypatch):
# patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
# Key error expected because a config is not passed, and the
# default is now missing the 'user' entry.
with pytest.raises(KeyError):
_ = app.create_connection_string()
fixture
的模块化使您可以灵活地为每个潜在的mock
定义单独的fixture
,并在所需的测试中引用它们。
# contents of test_app.py
import pytest
# app.py with the connection string function
import app
# all of the mocks are moved into separated fixtures
@pytest.fixture
def mock_test_user(monkeypatch):
"""Set the DEFAULT_CONFIG user to test_user."""
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
@pytest.fixture
def mock_test_database(monkeypatch):
"""Set the DEFAULT_CONFIG database to test_db."""
monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
@pytest.fixture
def mock_missing_default_user(monkeypatch):
"""Remove the user key from DEFAULT_CONFIG"""
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
# tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;"
result = app.create_connection_string()
assert result == expected
def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError):
_ = app.create_connection_string()
更多建议: