Grenzen des Mocking

Eines der größten Probleme bei der Verwendung von Mocks besteht darin, dass wir bei in einem Test nicht mehr das Verhalten, sondern die Implementierung testen. Dies ist jedoch nicht nur zeitaufwändig, sondern auch gefährlich: Ein gültiges Refactoring z.B. das Ändern eines Variablennamens, kann Tests zum Scheitern bringen, wenn diese bestimmte Variable gemockt wurde. Wir wollen jedoch, dass unsere Tests nur dann fehlschlagen, wenn es Brüche im Verhalten gibt, nicht jedoch nur bei Codeänderungen.

Manchmal ist Mocking jedoch der einfachste Weg, Exceptions oder Fehlerbedingungen zu erzeugen und sicherzustellen, dass euer Code diese korrekt behandelt. Es gibt auch Fälle, in denen das Testen von Verhalten unzumutbar ist, wie z.B. beim Zugriff auf eine Zahlungs-API oder beim Senden von E-Mails. In diesen Fällen ist es eine gute Option zu testen, ob euer Code eine bestimmte API-Methode zum richtigen Zeitpunkt und mit den richtigen Parametern aufruft.

Siehe auch

Bemerkung

Auch bei agentischer Software-Entwicklung versuchen wir, Mocking so weit wie möglich zu vermeiden:

AGENTS.md
- Prefer testing real code where possible. Use mocks and `monkeypatch` when absolute necessary. Try to avoid mocking as much as possible.

Siehe auch

Mocking vermeiden mit Tests auf mehreren Ebenen

Wir können die Tasks-CLI auch ohne Mocks testen indem wir auch die API verwenden. Dabei werden wir nicht die API testen, sondern sie nur verwenden, um das Verhalten von Aktionen zu überprüfen, die über die CLI ausgeführt werden. Das Beispiel test_add_with_owner können wir auch folgendermaßen testen:

def test_add_with_owner(tasks_db, tasks_cli):
    tasks_cli("add some task -o veit")
    expected = tasks.Task("some task", owner="veit", state="todo")
    all = tasks_db.list_tasks()
    assert len(all) == 1
    assert all[0] == expected

Mocking testet die Implementierung der Befehlszeilenschnittstelle und stellt sicher, dass ein API-Aufruf mit bestimmten Parametern erfolgt. Beim Mixed-Layer-Ansatz wird das Verhalten getestet, um sicherzustellen, dass das Ergebnis unseren Vorstellungen entspricht. Diese Ansatz ist viel weniger ein Change-Detector und hat eine größere Chance, während eines Refactorings gültig zu bleiben. Interessanterweise sind die Tests auch etwa doppelt so schnell:

$ uv run pytest -s tests/cli/test_add.py::test_add_with_owner
============================= test session starts ==============================
…
configfile: pyproject.toml
plugins: cov-4.1.0, Faker-19.11.0
collected 1 item

tests/cli/test_add.py .

============================== 1 passed in 0.03s ===============================

Wir könnten Mocking auch auf eine andere Weise vermeiden. Wir könnten das Verhalten vollständig über die CLI testen. Dazu müsste möglicherweise die Ausgabe der Tasks-Liste geparst werden, um den korrekten Datenbankinhalt zu überprüfen.

In der API gibt add_task() einen Index zurück und bietet eine get_task(index)()-Methode, die beim Testen hilft. Beide Methoden sind in der CLI nicht vorhanden, könnten es aber sein. Wir könnten vielleicht die Befehle tasks get index oder tasks info index hinzufügen, damit wir ein Task abrufen können, anstatt tasks list für alles verwenden zu müssen. list unterstützt auch bereits Filterung. Vielleicht würde das Filtern nach index funktionieren, anstatt einen neuen Befehl hinzuzufügen. Und wir könnten tasks add eine Ausgabe hinzufügen, die etwas sagt wie Task hinzugefügt bei Index 3. Diese Änderungen würden in die Kategorie Design for Testability fallen. Sie scheinen auch keine tiefen Eingriffe in die Schnittstelle zu sein und sollten vielleicht in zukünftigen Versionen berücksichtigt werden.