文章目录
2.7 如何进行修补/模拟 模块和环境
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)
有时测试需要调用依赖于全局设置的功能,或调用不能容易测试的代码,如网络访问.
猴子补丁装置可以帮助您安全地设置/删除属性、字典项或环境变量,或修改用于导入的系统路径
猴子补丁装置提供了这些安全补丁方法和模拟功能的辅助测试
所有的修改都将在请求的测试功能或夹具完成后被撤消,考虑以下场景:
- 修改函数的行为或类的属性,例如有一个API调用或数据库连接,但您知道期望的输出应该是什么,
使用monkeypatch.setattr可以用您所需的测试行为来修补该函数或属性,这可以包括您自己的功能,
使用 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().
注意:猴子补丁的一些参考博客:https://holgerkrekel.net/2009/03/03/monkeypatching-in-unit-tests-done-right/
2.7.1 猴子修补
考虑一个您正在使用用户目录的场景,在测试的上下文中,您不希望测试依赖于正在运行的用户,monkeypatch 可用于补丁函数,依赖于用户总是返回一个特定的值。
monkeypatch.setattr 修补 Path.home,因此,在运行测试时,将始终使用已知的测试路径路径(“/abc”)。
# 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")
2.7.2 猴子返回对象:构建模拟类
monkeypatch.setattr 可以与类一起使用来模拟从函数而不是值返回的对象,用一个简单的函数来获取一个APIurl并返回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()
为了达到测试的目的,我们需要模拟返回的响应对象r。r的模拟需要一个返回字典的.json()方法。这可以在我们的测试文件中通过定义一个类来表示r来实现。
# contents of test_app.py, a simple test for our API retrieval
import requests
import app
class MockResponse:
@staticmethod
def json():
return {"mock_key": "mock_response"}
def test_get_json(monkeypatch):
def mock_get(*args, **kwargs):
return MockResponse()
monkeypatch.setattr(requests, "get", mock_get)
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
猴子补丁中,使用固定装置:
# 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"}
@pytest.fixture
def mock_response(monkeypatch):
def mock_get(*args, **kwargs):
return MockResponse()
monkeypatch.setattr(requests, "get", mock_get)
def test_get_json(mock_response):
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
此外,如果模拟被设计为应用于所有测试,那么设备可以移动到conftest.py文件,并使用autouse=True 选项。
2.7.3 全局补丁示例:防止来自远程操作的“请求”
如果您想阻止“请求”库在所有测试中执行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")
每个测试功能都将执行此自动机装置,并删除该方法:request.session.
Session.request 以便在测试中创建http请求的任何尝试都将失败。
2.7.4 猴子修补环境变量
如果您正在使用环境变量,那么您通常需要安全地更改这些值或从系统中删除它们以进行测试,猴子补丁提供了一种机制来setenv 和 delenv方法:
# contents of 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环境变量不存在。使用猴子补丁,可以安全地测试这两个路径,而不影响运行环境:
# 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)
# 这里使用的是猴子补丁处理
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()
2.7.5 猴子修补字典
monkeypatch.setitem可用于在测试期间安全地将字典的值设置为特定的值。以这个简化的连接字符串为例:
# contents of app.py
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
import app
def test_connection(monkeypatch):
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
result = app.create_connection_string()
expected = "User Id=test_user; Location=test_db;"
assert result == expected
您可以使用monkeypatch.delitem来删除值:
# contents of test_app.py
import pytest
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()
夹具的模块化使您可以灵活地为每个潜在的模拟模型定义单独的夹具,并在所需的测试中引用它们:
# contents of test_app.py
import pytest
import app
# 所有的猴子被移动到固定装置
@pytest.fixture
def mock_test_user(monkeypatch):
"""设置 DEFAULT_CONFIG user to test_user."""
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
@pytest.fixture
def mock_test_database(monkeypatch):
"""设置 the DEFAULT_CONFIG database to test_db."""
monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
@pytest.fixture
def mock_missing_default_user(monkeypatch):
"""删除 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()