So wird man Module und Umgebungen mit Monkeypatching/Mocking behandelt¶
Manchmal müssen Tests Funktionalität aufrufen, die von globalen Einstellungen abhängt oder Code aufruft, der nicht einfach getestet werden kann, wie z. B. Netzwerkzugriff. Das monkeypatch-Fixture hilft Ihnen, Attribute, Dictionary-Elemente oder Umgebungsvariablen sicher zu setzen/löschen oder sys.path für Importzwecke zu ändern.
Das monkeypatch-Fixture bietet diese Hilfsmethoden zum sicheren Patchen und Mocken von Funktionalität in Tests
Alle Änderungen werden nach Beendigung der anfordernden Testfunktion oder des Fixtures rückgängig gemacht. Der Parameter raising bestimmt, ob ein KeyError oder AttributeError ausgelöst wird, wenn das Ziel der Setz-/Löschoperation nicht existiert.
Betrachten Sie die folgenden Szenarien
1. Modifizieren des Verhaltens einer Funktion oder der Eigenschaft einer Klasse für einen Test, z. B. gibt es einen API-Aufruf oder eine Datenbankverbindung, die Sie für einen Test nicht durchführen möchten, aber Sie wissen, was die erwartete Ausgabe sein sollte. Verwenden Sie monkeypatch.setattr, um die Funktion oder Eigenschaft mit Ihrem gewünschten Testverhalten zu patchen. Dies kann eigene Funktionen einschließen. Verwenden Sie monkeypatch.delattr, um die Funktion oder Eigenschaft für den Test zu entfernen.
2. Modifizieren der Werte von Dictionaries, z. B. Sie haben eine globale Konfiguration, die Sie für bestimmte Testfälle ändern möchten. Verwenden Sie monkeypatch.setitem, um das Dictionary für den Test zu patchen. monkeypatch.delitem kann zum Entfernen von Elementen verwendet werden.
3. Modifizieren von Umgebungsvariablen für einen Test, z. B. um das Programmverhalten zu testen, falls eine Umgebungsvariable fehlt, oder um mehrere Werte für eine bekannte Variable zu setzen. monkeypatch.setenv und monkeypatch.delenv können für diese Patches verwendet werden.
4. Verwenden Sie monkeypatch.setenv("PATH", value, prepend=os.pathsep), um $PATH zu ändern, und monkeypatch.chdir, um den Kontext des aktuellen Arbeitsverzeichnisses während eines Tests zu ändern.
5. Verwenden Sie monkeypatch.syspath_prepend, um sys.path zu ändern, was auch pkg_resources.fixup_namespace_packages und importlib.invalidate_caches() aufruft.
6. Verwenden Sie monkeypatch.context, um Patches nur in einem bestimmten Geltungsbereich anzuwenden, was bei der Bereinigung komplexer Fixtures oder Patches für die Standardbibliothek helfen kann.
Siehe den Blogbeitrag zu Monkeypatching für einführendes Material und eine Diskussion über dessen Motivation.
Monkeypatching von Funktionen¶
Betrachten Sie ein Szenario, in dem Sie mit Benutzerverzeichnissen arbeiten. Im Testkontext möchten Sie nicht, dass Ihr Test vom laufenden Benutzer abhängt. monkeypatch kann verwendet werden, um von Benutzern abhängige Funktionen zu patchen, damit sie immer einen bestimmten Wert zurückgeben.
In diesem Beispiel wird monkeypatch.setattr verwendet, um Path.home zu patchen, sodass der bekannte Testpfad Path("/abc") immer verwendet wird, wenn der Test ausgeführt wird. Dies entfernt jegliche Abhängigkeit vom laufenden Benutzer für Testzwecke. monkeypatch.setattr muss aufgerufen werden, bevor die Funktion, die die gepatchte Funktion verwendet, aufgerufen wird. Nach Beendigung der Testfunktion wird die Änderung von Path.home rückgängig gemacht.
# 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 von zurückgegebenen Objekten: Mock-Klassen erstellen¶
monkeypatch.setattr kann in Verbindung mit Klassen verwendet werden, um von Funktionen zurückgegebene Objekte anstelle von Werten zu mocken. Stellen Sie sich eine einfache Funktion vor, die eine API-URL nimmt und die JSON-Antwort zurückgibt.
# 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()
Wir müssen r, das zurückgegebene Antwortobjekt, für Testzwecke mocken. Der Mock von r benötigt eine Methode .json(), die ein Dictionary zurückgibt. Dies kann in unserer Testdatei geschehen, indem wir eine Klasse definieren, die r repräsentiert.
# 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 wendet den Mock für requests.get mit unserer Funktion mock_get an. Die Funktion mock_get gibt eine Instanz der Klasse MockResponse zurück, die eine definierte Methode json() hat, um ein bekanntes Testdictionary zurückzugeben und keine externe API-Verbindung benötigt.
Sie können die Klasse MockResponse mit dem entsprechenden Grad an Komplexität für das zu testende Szenario erstellen. Sie könnte zum Beispiel eine Eigenschaft ok enthalten, die immer True zurückgibt, oder je nach Eingabestrings unterschiedliche Werte von der gemockten Methode json() zurückgeben.
Dieser Mock kann über Tests hinweg mit einem fixture geteilt werden
# 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"
Darüber hinaus, wenn der Mock so konzipiert war, dass er auf alle Tests angewendet wird, könnte das fixture in eine conftest.py-Datei verschoben und mit der Option autouse=True verwendet werden.
Beispiel für globalen Patch: Verhindern von Remote-Operationen mit "requests"¶
Wenn Sie verhindern möchten, dass die Bibliothek "requests" HTTP-Anfragen in all Ihren Tests durchführt, können Sie dies tun
# 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")
Dieses Autouse-Fixture wird für jede Testfunktion ausgeführt und löscht die Methode request.session.Session.request, sodass alle Versuche, HTTP-Anfragen innerhalb von Tests zu erstellen, fehlschlagen.
Hinweis
Seien Sie vorsichtig: Es wird nicht empfohlen, eingebaute Funktionen wie open, compile usw. zu patchen, da dies die Interna von pytest unterbrechen kann. Wenn dies unvermeidlich ist, können die Optionen --tb=native, --assert=plain und --capture=no helfen, obwohl es keine Garantie gibt.
Hinweis
Beachten Sie, dass das Patchen von Funktionen der Standardbibliothek und einiger von pytest verwendeter Drittanbieterbibliotheken pytest selbst unterbrechen kann. Daher wird in solchen Fällen empfohlen, MonkeyPatch.context() zu verwenden, um das Patchen auf den zu testenden Block zu beschränken.
import functools
def test_partial(monkeypatch):
with monkeypatch.context() as m:
m.setattr(functools, "partial", 3)
assert functools.partial == 3
Siehe #3290 für Details.
Monkeypatching von Umgebungsvariablen¶
Wenn Sie mit Umgebungsvariablen arbeiten, müssen Sie oft deren Werte sicher ändern oder sie für Testzwecke aus dem System löschen. monkeypatch bietet einen Mechanismus, um dies mit den Methoden setenv und delenv zu tun. Unser Beispielcode zum Testen
# 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()
Es gibt zwei mögliche Pfade. Erstens ist die Umgebungsvariable USER auf einen Wert gesetzt. Zweitens existiert die Umgebungsvariable USER nicht. Mit monkeypatch können beide Pfade sicher getestet werden, ohne die laufende Umgebung zu beeinträchtigen.
# 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()
Dieses Verhalten kann in fixture-Strukturen verschoben und über Tests hinweg geteilt werden.
# 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 von Dictionaries¶
monkeypatch.setitem kann verwendet werden, um Werte von Dictionaries während Tests sicher auf spezifische Werte zu setzen. Nehmen Sie dieses vereinfachte Verbindungszeichenfolgenbeispiel
# 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']};"
Für Testzwecke können wir das Dictionary DEFAULT_CONFIG auf spezifische Werte patchen.
# 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
Sie können monkeypatch.delitem verwenden, um Werte zu entfernen.
# 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()
Die Modularität von Fixtures gibt Ihnen die Flexibilität, separate Fixtures für jeden möglichen Mock zu definieren und sie in den benötigten Tests zu referenzieren.
# 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()
API-Referenz¶
Schauen Sie in die Dokumentation für die Klasse MonkeyPatch.