Beispiele¶
Mocking von datetime.datetime¶
Zunächst wollten wir mit einem einfachen Beispiel starten und überprüfen, ob die Arbeitstage von Montag bis Freitag korrekt ermittelt werden.
Zunächst importieren wir
datetime.datetimeundMock:1from datetime import datetime 2from unittest.mock import Mock
Dann definieren wir zwei Testtage:
5monday = datetime(year=2021, month=10, day=11) 6saturday = datetime(year=2021, month=10, day=16)
Nun definieren wir eine Methode zur Überprüfung der Arbeitstage, wobei die datetime-Bibliothek von Python Montage als
0und Sonntage als6behandelt:9def is_workingday(): 10 today = datetime.today() 11 return 0 <= today.weekday() < 5
Dann mocken wir datetime:
14datetime = Mock()
Schließlich testen wir unsere beiden Mock-Objekte:
17def test_workinngday(): 18 # Mock .today() to return Tuesday 19 datetime.today.return_value = monday 20 # Test Tuesday is a weekday 21 assert is_workingday()
24def test_no_workingday(): 25 # Mock .today() to return Saturday 26 datetime.today.return_value = saturday 27 # Test Saturday is not a weekday 28 assert not is_workingday()
Mocking der CLI¶
Für die Tests der Tasks-CLI werden wir uns auch ansehen, wie der von Typer bereitgestellte CliRunner beim Testen hilft.
Typer bietet eine Testschnittstelle, womit wir unsere Anwendung aufrufen können,
ohne, wie in dem kurzen capsys-Beispiel auf
subprocess.run() zurückgreifen zu müssen. Das ist gut, weil wir
nicht simulieren können, was in einem separaten Prozess läuft. So können wir in
tests/cli/conftest.py der invoke()-Funktion unseres runner nur
unsere Anwendung cusy.tasks.cli.app und eine Liste von Strings übergeben,
die den Befehl darstellt: genauer wandeln wir mit
shlex.split(command_string)() die Befehle, z. B.
list -o "veit" in ["list", "-o", "veit"] um und können die
Ausgabe dann abfangen und zurückgeben.
import shlex
import pytest
from typer.testing import CliRunner
from cusy import tasks
runner = CliRunner()
@pytest.fixture()
def tasks_cli(db_path, monkeypatch, tasks_db):
monkeypatch.setenv("ITEMS_DB_DIR", db_path.as_posix())
def run_cli(command_string):
command_list = shlex.split(command_string)
result = runner.invoke(tasks.cli.app, command_list)
output = result.stdout.rstrip()
return output
return run_cli
Anschließend können wir diese Fixture einfach verwenden um z.B. die Version in tests/cli/test_version.py zu testen:
from cusy import tasks
def test_version(tasks_cli):
assert tasks_cli("version") == tasks.__version__
Siehe auch
Mocking von Attributen¶
Schauen wir uns an, wie wir Mocking verwenden können, um sicherzustellen, dass
z. B. auch dreistellige Versionsnummern von
tasks.__version__() korrekt über die CLI ausgegeben werden. Hierfür werden
wir mock.patch.object() als Kontextmanager verwenden:
from unittest import mock
from cusy import tasks
def test_mock_version(tasks_cli):
with mock.patch.object(tasks, "__version__", "100.0.0"):
assert tasks_cli("version") == tasks.__version__
In unserem Testcode importieren wir tasks. Das resultierende tasks-Objekt
ist das, was wir patchen werden. Der Aufruf von mock.patch.object(), der
als Kontextmanager innerhalb eines
with-Blocks verwendet wird, gibt ein Mock-Objekt zurück, das nach dem
with-Block aufgeräumt wird:
In diesem Fall wird das Attribut
__version__vontasksfür die Dauer deswith-Blocks durch"100.0.0"ersetzt.Anschließend verwenden wir
tasks_cli(), um unsere CLI-Anwendung mit dem Befehl"version"aufzurufen. Wenn die Methodeversion()aufgerufen wird, ist das Attribut__version__jedoch nicht der ursprüngliche String, sondern der String, den wir mitmock.patch.object()ersetzt haben.
Mocking von Klassen und Methoden¶
In src/cusy/tasks/cli.py haben wir config() folgendermaßen
definiert:
def config():
"""List the path to the Tasks db."""
with tasks_db() as db:
print(db.path())
tasks_db() ist ein Kontextmanager, der
ein tasks.TasksDB-Objekt zurückgibt. Das zurückgegebene Objekt wird dann als
db verwendet, um db.path() aufzurufen. Wir sollten hier also zwei
Dinge zu mocken: tasks.TasksDB und eine seiner Methoden, path().
Beginnen wir mit der Klasse:
from unittest import mock
from cusy import tasks
def test_mock_tasksdb(tasks_cli):
with mock.patch.object(tasks, "TasksDB") as MockTasksDB:
mock_db_path = MockTasksDB.return_value.path.return_value = "/foo/"
assert tasks_cli("config") == str(mock_db_path)
Lasst und sicherstellen, dass es wirklich funktioniert:
$ uv run pytest -v -s tests/cli/test_config.py::test_mock_tasksdb
============================= test session starts ==============================
...
configfile: pyproject.toml
plugins: cov-4.1.0, Faker-19.11.0
collected 1 item
tests/cli/test_config.py::test_mock_tasksdb PASSED
============================== 1 passed in 0.04s ===============================
Prima, nun müssen wir nur noch den Mock für die Datenbank in eine Fixture verschieben, denn wir werden ihn in vielen Testmethoden brauchen:
@pytest.fixture()
def mock_tasksdb():
with mock.patch.object(tasks, "TasksDB") as MockTasksDB:
yield MockTasksDB.return_value
Diese Fixture mockt das TasksDB-Objekt und gibt den return_value zurück,
so dass Tests ihn verwenden können, um Dinge wie path zu ersetzen:
def test_mock_tasksdb(tasks_cli, mock_tasksdb):
mock_tasksdb.path.return_value = "/foo/"
result = runner.invoke(app, ["config"])
assert result.stdout.rstrip() == "/foo/"
Alternativ kann zum Mocken von Klassen oder Objekten auch der
@mock.patch()-Dekorator verwendet
werden. In den folgenden Beispielen wird die Ausgabe von os.listdir gemockt.
Dazu muss db_path nicht im Dateisystem vorhanden sein:
import os
from unittest import mock
@mock.patch("os.listdir", mock.MagicMock(return_value="db_path"))
def test_listdir():
assert "db_path" == os.listdir()
Eine weitere Alternative ist, den Rückgabewert separat zu definieren:
@mock.patch("os.listdir")
def test_listdir(mock_listdir):
mock_listdir.return_value = "db_path"
assert "db_path" == os.listdir()