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