有時測試需要調(diào)用依賴于全局設(shè)置的功能或調(diào)用不容易測試的代碼,例如網(wǎng)絡(luò)訪問。 ?monkeypatch fixture
?可幫助您安全地設(shè)置/刪除屬性、字典項或環(huán)境變量,或修改 ?sys.path
? 以進行導(dǎo)入。
?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
參數(shù)確定如果設(shè)置/刪除操作的目標(biāo)不存在,是否會引發(fā) ?KeyError
或 ?AttributeError
?
考慮以下場景:
monkeypatch.setenv("PATH", value, prepend=os.pathsep)
? 修改 ?$PATH
?,并使用 ?monkeypatch.chdir
? 在測試期間更改當(dāng)前工作目錄的上下文。monkeypatch.syspath_prepend
? 修改 ?sys.path
?,它還將調(diào)用 ?pkg_resources.fixup_namespace_packages
? 和 ?importlib.invalidate_caches()
?考慮一個使用用戶目錄的場景。 在測試的上下文中,您不希望您的測試依賴于正在運行的用戶。 ?monkeypatch
?可用于修補依賴于用戶的函數(shù)以始終返回特定值。
在此示例中,?monkeypatch.setattr
? 用于修補 ?Path.home
?,以便在運行測試時始終使用已知的測試路徑 ?Path("/abc")
?。 這消除了出于測試目的對運行用戶的任何依賴。 必須在調(diào)用將使用修補函數(shù)的函數(shù)之前調(diào)用 ?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")
?monkeypatch.setattr
?可以與類結(jié)合使用,以模擬從函數(shù)返回的對象而不是值。 想象一個簡單的函數(shù)來獲取 API url 并返回 json 響應(yīng)。
# 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
?,返回的響應(yīng)對象用于測試目的。 ?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
?函數(shù)對 ?requests.get
? 應(yīng)用模擬。 ?mock_get
函數(shù)返回一個 ?MockResponse
類的實例,它定義了一個 ?json()
? 方法來返回一個已知的測試字典,并且不需要任何外部 API 連接。
您可以為您正在測試的場景構(gòu)建具有適當(dāng)復(fù)雜程度的 ?MockResponse
類。 例如,它可以包含一個始終返回 ?True
的 ?ok
屬性,或者根據(jù)輸入字符串從 ?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
?被設(shè)計為應(yīng)用于所有測試,則可以將?fixture
?移動到?conftest.py
?文件并使用autuse =True
?選項。
如果你想阻止?requests
?庫在所有測試中執(zhí)行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")
將為每個測試函數(shù)執(zhí)行此 ?autouse fixture
?,并將刪除方法 ?request.session.Session.request
? 以便測試中創(chuàng)建 http 請求的任何嘗試都將失敗。
請注意,不建議修補內(nèi)置函數(shù),例如 ?open
?、?compile
等,因為它可能會破壞 pytest 的內(nèi)部結(jié)構(gòu)。 如果這是不可避免的,傳遞 ?--tb=native
?、?--assert=plain
? 和 ?--capture=no
? 可能會有所幫助,盡管不能保證。
請注意,pytest使用的?stdlib
?函數(shù)和一些第三方庫補丁可能會破壞pytest本身,因此在這些情況下,建議使用?MonkeyPatch.context()
?來限制補丁到你想要測試的塊:
import functools
def test_partial(monkeypatch):
with monkeypatch.context() as m:
m.setattr(functools, "partial", 3)
assert functools.partial == 3
如果您正在使用環(huán)境變量,那么為了測試的目的,您經(jīng)常需要安全地更改這些值或從系統(tǒng)中刪除它們。?Monkeypatch
?提供了一種使用?setenv
?和?delenv
?方法來實現(xiàn)這一點的機制。例如:
# 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
?環(huán)境變量設(shè)置為一個值。 其次,?USER
?環(huán)境變量不存在。 使用 ?monkeypatch
?可以安全地測試兩個路徑,而不會影響運行環(huán)境:
# 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()
這種行為可以轉(zhuǎn)移到?fixture
?結(jié)構(gòu)中,并在測試中共享:
# 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()
?monkeypatch.setitem
? 可用于在測試期間將字典的值安全地設(shè)置為特定值。 以這個簡化的連接字符串為例:
# 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()
更多建議: