Integrationstests & Testabdeckung (Coverage) messen
Unit-Tests prüfen einzelne Bausteine. Aber ob die Bausteine zusammen funktionieren, sagt kein Unit-Test. Dafür braucht es Integrationstests — und um zu wissen, was überhaupt getestet ist, misst man Coverage. Beides mit Augenmaß.
Unit vs. Integration vs. E2E
- Unit: eine Funktion/Klasse isoliert, alles andere gemockt. Schnell, viele davon.
- Integration: mehrere Teile zusammen — z.B. View + Service + echte DB. Langsamer, gezielter.
- E2E: das ganze System durch die UI/API. Am langsamsten, wenige davon.
Die Testpyramide: viele schnelle Unit-Tests unten, weniger Integrationstests in der Mitte, ganz wenige E2E-Tests oben. Wer sie umdreht, bekommt eine langsame, brüchige Suite.
Integrationstests schreiben
In Django ist ein View-Test mit echter DB und echten Services bereits ein Integrationstest — er prüft das Zusammenspiel:
@pytest.mark.django_db
def test_bestellung_anlegen_end_to_end(client, user):
client.force_login(user)
# Kompletter Fluss: Request -> View -> Service -> DB
response = client.post("/bestellung/", {
"produkt": "SKU-123", "menge": 2,
})
assert response.status_code == 302
# Wirkte sich alles korrekt aus?
bestellung = Bestellung.objects.get(user=user)
assert bestellung.positionen.count() == 1
assert bestellung.gesamtpreis > 0Hier wird bewusst nicht gemockt — der Wert liegt gerade darin, dass echte Teile zusammenspielen. Externe Dienste (Zahlungsanbieter etc.) mockt man aber weiterhin.
Coverage mit coverage.py
pip install pytest-cov
# Coverage beim Testlauf messen
pytest --cov=myapp --cov-report=term-missing
# HTML-Report (zeigt Zeile für Zeile)
pytest --cov=myapp --cov-report=html
# -> öffne htmlcov/index.html# .coveragerc - was nicht zählt
[run]
omit =
*/migrations/*
*/tests/*
*/__init__.py
manage.py
*/settings/*
[report]
exclude_lines =
pragma: no cover
raise NotImplementedError
if __name__ == .__main__.:Coverage-Reports lesen
Der term-missing-Report zeigt genau, welche Zeilen nie ausgeführt wurden:
Name Stmts Miss Cover Missing
-----------------------------------------------
myapp/services.py 45 3 93% 28-30
myapp/views.py 60 8 87% 15, 42-48
-----------------------------------------------
TOTAL 105 11 90%Die Spalte Missing ist das Wertvolle: Zeile 28-30 in services.py ist ungetestet — ist das ein wichtiger Fehlerpfad? Dann Test schreiben. Ein selten genutzter Logging-Zweig? Dann vielleicht nicht.
Die 100%-Falle
100% Coverage klingt gut, ist aber ein Trugschluss: Es sagt nur, dass jede Zeile ausgeführt wurde — nicht, dass sie korrekt ist oder die richtigen Assertions geprüft wurden.
# 100% Coverage, aber nutzloser Test:
def test_gesamtpreis():
berechne_preis([{"preis": 10}]) # kein assert!
# Zeile ausgeführt = "covered", aber nichts geprüftCoverage zeigt, was nicht getestet ist — das ist nützlich. Aber eine hohe Zahl beweist keine Qualität. 80-90% mit sinnvollen Assertions ist mehr wert als 100% ohne.
Coverage in CI durchsetzen
# In der CI-Pipeline: Mindest-Coverage erzwingen
pytest --cov=myapp --cov-fail-under=80
# -> Pipeline schlägt fehl, wenn unter 80%
# Aber: als Untergrenze gegen Verschlechterung,
# nicht als Jagd auf die letzte Zeile.Fazit
Integrationstests sichern das Zusammenspiel, das Unit-Tests nicht sehen — sparsam eingesetzt nach dem Pyramidenprinzip. Coverage ist ein Kompass, kein Ziel: Sie zeigt ungetestete Pfade, aber die Zahl allein sagt nichts über Qualität. Sinnvolle Assertions bei 85% schlagen leere Tests bei 100%.
