Files
Projektwissen/docs/konzept/07_qgis_tests_plan.md
T
AndreasTharangandClaude Sonnet 5 ab416fb34c Projektwissen: Git-Vorgaben, Ticketzugriff, Tests und Einstieg
Erster Stand des Repositories AG_QGIS/Projektwissen (bisher Arbeitstitel
"Workspace", ADR 0006). Der lokale Ordner heisst weiter AG_QGIS, weil darin
auch die Plugin-Klone liegen.

Git verbindlich (ADR 0007), aus dem bisherigen Wiki-Konzept entwickelt:
- docs/leitfaden/05_git_arbeitsweise.md mit Kanal-Kette, Branch-Namen und
  Gitea-Branch-Schutz.
- Der Einstiegspunkt eines Fixes richtet sich nach dem Kanal, in dem der
  Fehler sitzt; ein Fix an testing oder stable ist erst mit der
  Rueckfuehrung nach unten abgeschlossen. Das fehlte bisher.
- Der stabile Kanal heisst stable, nicht main: der Release-Workflow in
  Repository:hidden/workflows fuehrt git checkout "$CHANNEL" aus, der
  Branchname muss also dem Kanalnamen entsprechen. Er existiert in keinem
  Plugin-Repo und ist vom Maintainer anzulegen.
- Der Mensch drueckt ab: .claude/settings.json sperrt push, merge, tag.
- scripts/hallo.py und /hallo holen den Stand aller Klone, per
  Fast-Forward wo gefahrlos; nie mergen, stashen oder verwerfen.

Ticketzugriff:
- scripts/gitea.py liest Issues, Kommentare und PRs ueber die oeffentliche
  API, ohne Token, nur GET. Schreibendes nur als Entwurf in entwuerfe/.
- Skill sn-ticket.

Tests (ADR 0009):
- docs/leitfaden/06_tests.md: Testebenen, Testpflicht, und wie eine
  Fachperson beurteilt, ob Tests fachlich sinnvoll sind.
- scripts/testkatalog.py erzeugt docs/tests/testkatalog.md per ast-Parsing,
  ohne Import und ohne QGIS. Befund: nur 100 von 332 Tests haben einen
  Docstring, sn_basis keinen einzigen von 87.
- Vorlage und erste Testkonzept-Seite mit Mermaid. Dabei gefunden und gegen
  origin geprueft: _ermittle_gebietstyp_aus_fbschl hat keinen Test, obwohl
  PR #44/#45 genau diese Funktion tolerant gemacht hat.
- docs/konzept/07_qgis_tests_plan.md: Stufenplan fuer Tests gegen echtes
  QGIS. Dabei aufgefallen: QGIS 3.40 ist seit Januar 2026 End of Life,
  qgis_min steht aber noch darauf.

Werkzeuge (ADR 0008):
- 04_werkzeuge_einrichten.md nur noch Claude Code als CLI und in VS Code.
- .vscode/ entfernt, brauchbare Einstellungen als persoenliche Vorlage.

Ausserdem: README als Einstieg neu aufgebaut, Mermaid-Diagramm in
schnittstellen.md repariert (unmaskierte Klammern, rendert seither nicht).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 11:57:26 +02:00

13 KiB
Raw Blame History

Plan: automatisierte Tests gegen echtes QGIS

Stand 04.09.2026. Vorschlag zur Abstimmung im Team. Ergänzt ../leitfaden/06_tests.md um die zweite Testebene. Recherchestand und Quellen am Ende.

1. Worum es geht

Heute laufen 332 Tests im Mock-Modus — ohne QGIS, in unter einer Sekunde. Das deckt Logik, Parsing und Prüfregeln ab. Es deckt nicht ab, was erst im Zusammenspiel mit QGIS passiert: ob ein Layer wirklich entsteht, ob ein WFS-Ergebnis verarbeitbar ist, ob ein Drucklayout eine Seite erzeugt, ob ein Dock sich sauber wieder abmeldet. Genau dort sitzen die Fehler, die heute nur durch manuelles Klicken auffallen — und die zwischen Qt5 und Qt6 unterschiedlich ausfallen.

Es gibt seit Jahren einen etablierten Weg, QGIS-Plugins ohne Bildschirm und ohne menschliche Interaktion gegen eine echte QGIS-Instanz zu testen. Dieser Plan beschreibt, wie wir dort in vier Stufen hinkommen.

Der Mock-Modus bleibt. Er wird nicht ersetzt. Er ist die schnelle Ebene, die bei jedem Commit läuft; QGIS-Tests sind die langsame Ebene, die bei jedem Pull Request läuft. Wer die Mocks abschafft, tauscht Sekunden gegen Minuten und verliert die Wrapper-Disziplin, die den Qt5/Qt6-Code zusammenhält.

2. Vorab: unsere Zielversionen stimmen nicht mehr

Recherchestand 04.09.2026, geprüft am QGIS-Release-Fahrplan und an den Docker-Tags:

Version Status
LTR 3.44 aktueller Langzeit-Release
Stable 4.2 wird am 25.09.2026 zum LTR
3.40 3.40.15 (Januar 2026) End of Life, keine Updates mehr

workspace.toml sagt qgis_min = "3.40", alle .gitea/templates/metadata.template sagen qgisMinimumVersion=3.40. Das ist keine Testfrage, sondern eine Produktentscheidung: Welche QGIS-Versionen unterstützen wir? Sie muss vor Stufe 2 fallen, weil sie die Test-Matrix bestimmt. Vorschlag zur Diskussion: 3.44 als Minimum, 4.2 als zweites Matrix-Ziel, 3.40 nur solange Bestandsinstallationen es erzwingen.

3. Wie es technisch funktioniert

Der Kern ist qgis.testing.start_app() aus dem QGIS-Quellbaum. Es baut eine vollständige QgsApplication mit registrierten Datenprovidern auf, in einem temporären Profil, und ist idempotent — mehrfacher Aufruf schadet nicht. Plugins lädt es bewusst nicht; das eigene Plugin instanziiert der Test selbst.

import os
if not os.getenv("DISPLAY"):
    os.environ["QT_QPA_PLATFORM"] = "offscreen"   # muss VOR dem App-Start gesetzt sein

from qgis.testing import start_app, unittest
from qgis.testing.mocked import get_iface

QGIS_APP = start_app()
IFACE = get_iface()          # echtes QMainWindow, echter QgsMapCanvas, echter LayerTree

get_iface() liefert kein selbstgebautes Mock, sondern ein QgisInterface-Objekt mit echtem Hauptfenster, echtem Kartenfenster und echtem Layerbaum. Das ist deutlich realitätsnäher als alles, was wir selbst nachbauen könnten, und es kostet uns keine Pflege.

QT_QPA_PLATFORM=offscreen ist der Schalter, der das Ganze ohne Bildschirm laufen lässt — auf dem CI-Server genauso wie unter Windows.

4. Der Stufenplan

flowchart LR
    S1["<b>Stufe 1</b><br/>start_app() lokal<br/><i>klein</i>"]
    S2["<b>Stufe 2</b><br/>Gitea Actions<br/>Matrix 3.44 / 4.2<br/><i>mittel</i>"]
    S3["<b>Stufe 3</b><br/>pytest-qgis<br/><i>klein</i>"]
    S4["<b>Stufe 4</b><br/>Render-Vergleich<br/>Plan41<br/><i>groß</i>"]

    S1 --> S2 --> S3 --> S4

    style S1 fill:#e8f6ec,stroke:#2f9e44
    style S2 fill:#e6f2ff,stroke:#2b7dd9
    style S3 fill:#e6f2ff,stroke:#2b7dd9
    style S4 fill:#fff4e6,stroke:#d9822b

Stufe 1 — QGIS-Tests lokal, ohne CI

Ziel: Eine erste echte QGIS-Suite je Plugin, die ein Entwickler auf seinem Windows-Rechner starten kann.

  • Neuer Ordner tests/qgis/ je Plugin-Repo, getrennt von den Mock-Tests in tests/. Beide Suiten bleiben unabhängig lauffähig.
  • Start unter Windows über die OSGeo4W-Umgebung: C:\OSGeo4W\bin\python-qgis-ltr.bat -m pytest tests\qgis -v. Ein normales python -m pytest findet die QGIS-Bindings nicht.
  • Die vorhandenen Skripte tests/test_qgis.bat und tests/start_osgeo4w_qgis.bat werden dabei ersetzt. Sie enthalten heute fest verdrahtete Pfade (D:\OSGeo, D:\QGISQT5) und funktionieren nur auf einem Rechner.
  • Erste Kandidaten, weil dort der Mock-Modus am wenigsten aussagt: Layer anlegen und stylen (sn_basis), Verarbeitung einer WFS-Antwort (sn_verfahrensgebiet), Erzeugen eines Drucklayouts (sn_plan41).

Aufwand: klein. Kein neues Werkzeug, keine Infrastruktur. Nutzen: sofort, aber nur für die, die es lokal ausführen.

Stufe 2 — Gitea Actions mit Docker

Ziel: Jeder Pull Request wird automatisch gegen zwei QGIS-Generationen geprüft, ohne dass jemand eine zweite QGIS-Version installiert.

Das ist der größte Hebel des ganzen Plans. Der Qt5/Qt6-Unterschied ist in fallstricke.md ein Dauerthema, und er lässt sich mit Bordmitteln kaum abdecken — hier fällt er automatisch auf.

Die offiziellen Images qgis/qgis bringen QGIS samt Bindings mit. Verifizierte Tags (Stand 04.09.2026): 3.44, 3.44.14, 4.0, 4.2, 4.2.2, ltr, stable. QGIS 4 gibt es nur auf Qt6-Distributionen (Debian trixie, Ubuntu questing) — kein 4.x-noble.

name: QGIS-Tests
on:
  pull_request:
    branches: [unstable, testing, stable]

jobs:
  qgis:
    runs-on: ubuntu-latest        # Label muss auf einen Docker-fähigen act_runner zeigen
    strategy:
      fail-fast: false
      matrix:
        qgis: ["3.44", "4.2"]
    steps:
      - uses: actions/checkout@v4
      - name: Tests in QGIS ${{ matrix.qgis }}
        run: |
          docker run --rm -v "$PWD":/src -w /src \
            -e QT_QPA_PLATFORM=offscreen \
            qgis/qgis:${{ matrix.qgis }} \
            bash -c "python3 -m venv /venv --system-site-packages \
                     && /venv/bin/pip install -q pytest \
                     && /venv/bin/pytest tests/qgis -v"

Zu klären vor der Umsetzung:

  • Der vorhandene Gitea-Runner trägt das Label alpine-latest (siehe docs/vorlagen/gitea_workflow_tests.yaml). Für Docker-in-Docker braucht es einen Runner mit Docker-Zugriff. Das ist eine Frage an den Maintainer der Runner-Infrastruktur, keine Codefrage.
  • Die Fachplugins brauchen im Container auch sn_basis. Das Muster steckt schon in docs/vorlagen/gitea_workflow_tests.yaml: die Nachbar-Repos dazuklonen und unter ihren Paketnamen ablegen.

Aufwand: mittel, im Wesentlichen Infrastruktur. Nutzen: hoch und dauerhaft.

Stufe 3 — pytest-qgis

Ziel: Die bekannten Stolperfallen von QGIS-Tests nicht selbst lösen müssen.

pytest-qgis (Version 4.1.1, August 2026, Python ≥ 3.10, QGIS ≥ 3.34, Qt6-tauglich seit 3.0.0) bringt mit:

Was Warum wichtig
Fixture qgis_app startet die App einmal je Sitzung
Fixture qgis_iface patcht qgis.utils.iface automatisch — sonst ist es None
Fixture qgis_new_project leeres Projekt je Test; sonst wirken Layer aus Test A in Test B nach
automatisches Layer-Aufräumen verhindert die bekannten Abstürze beim Beenden
Fixture qgis_processing Processing-Registry initialisiert
--qgis_disable_exit Notausgang, falls es beim Beenden doch kracht

Aufwand: klein — etwa eine halbe Stunde plus Umstellung der bis dahin geschriebenen Tests. Empfehlung: erst nach Stufe 2, damit die Umstellung einmal an einer laufenden CI verifiziert wird.

Stufe 4 — Render-Vergleich für Plan41

Ziel: Erkennen, wenn sich das Aussehen eines Plans ungewollt ändert.

Für ein Plugin, dessen Zweck das Zeichnen von Plänen nach § 41 FlurbG ist, ist das fachlich der wertvollste Testtyp — und der teuerste. Mit einem verifizierten Fallstrick:

QgisTestCase.render_layout_check() und render_map_settings_check() suchen Referenzbilder über setControlPathPrefix() in einem zur Compile-Zeit einkompilierten Pfad des QGIS-Quellbaums. In einer installierten QGIS-Version existiert dieser Pfad nicht. Die bequemen Helfer sind für Plugins nicht nutzbar.

Gangbar sind stattdessen:

  • QgsLayoutExporter.renderPageToImage() und ein eigener, toleranter Bildvergleich, oder
  • QgsRenderChecker direkt, das als einziges setControlImagePath() anbietet.

Bedingungen, ohne die es nur Ärger macht:

  • Referenzbilder im CI-Container erzeugen, nie auf einem Entwickler-Windows. Schriftrendering und Antialiasing unterscheiden sich zwischen den Plattformen und erzeugen sonst dauerhaft rote Tests.
  • Toleranz einplanen (Farbabweichung und Anteil abweichender Pixel), nicht auf Byte-Gleichheit prüfen.
  • Mit zwei bis drei Referenzplänen anfangen, nicht mit fünfzig.

Aufwand: groß. Erst angehen, wenn Stufen 1–3 stabil laufen.

5. Was wir bewusst nicht machen

Ansatz Warum nicht
YAML-Testframework für Processing-Algorithmen An nose2, utilities.py und CTest im QGIS-Quellbaum gebunden. Für externe Plugins nur mit Portierungsaufwand nutzbar. Stattdessen processing.run(...) und assertLayersEqual.
qgis_setup.sh / qgis_testrunner.sh (Plugin im echten QGIS laden) Kein pytest-Reporting; Erfolg wird über String-Suche in der Standardausgabe erkannt. Nur nötig, wenn der Plugin-Ladevorgang selbst geprüft werden soll.
pytest-xdist (Parallelisierung) Bei unserer Teamgröße und Suite-Laufzeit unnötige Komplexität.
Eigenes Docker-Image bauen qgis/qgis plus pip install im Job reicht.
pb_tool Deployment-Helfer, kein Testwerkzeug. Lokal Geschmackssache.

6. Bekannte Fallstricke

Wandern bei der Umsetzung nach docs/wissen/fallstricke.md:

  • QT_QPA_PLATFORM muss vor dem Erzeugen der QgsApplication gesetzt sein. Danach wirkt es nicht mehr.
  • qgis.utils.iface ist nach start_app() None. Entweder pytest-qgis verwenden oder selbst qgis.utils.iface = get_iface() setzen — und darauf achten, keine Module zu früh zu importieren.
  • QgsProject.instance() ist global. Ohne Aufräumen zwischen den Tests beeinflussen sie sich gegenseitig.
  • Abstürze beim Beenden sind ein bekanntes QGIS-Thema, wenn Layer- oder Canvas-Objekte noch referenziert werden. Gegenmittel: pytest-qgis oder --qgis_disable_exit.
  • offscreen hat keinen Fenstermanager. Fokus, activeWindow() und Drag-and-drop verhalten sich anders. Für Widget-Tests notfalls xvfb-run -a davorsetzen (nur Linux).
  • Modale Dialoge blockieren den Test. Reihenfolge der Gegenmittel: den modalen Aufruf per monkeypatch ersetzen; open() statt exec() verwenden; erst als letztes QTimer.singleShot mit einem Klick-Handler.
  • Locale. Die Container laufen mit LANG=C.UTF-8, Entwickler-Windows meist auf Deutsch. Tests, die formatierte Zahlen oder Datumsangaben vergleichen, brechen sonst nur an einem der beiden Orte.
  • QGIS 4 / PyQt6. exec_() gibt es nicht mehr, Enum-Kurzformen (Qt.UserRole) auch nicht. Das trifft Testcode genauso wie Produktivcode. Für den Umstieg gibt es das Prüfwerkzeug pyqgis4-checker.

7. Vorschlag zur Reihenfolge

  1. Entscheidung zu den Zielversionen (Abschnitt 2) — blockiert Stufe 2, eigenes Issue.
  2. Stufe 1 an einem Plugin als Machbarkeitsnachweis, am besten sn_basis (keine Abhängigkeiten).
  3. Runner-Frage klären — hat der Gitea-Runner Docker?
  4. Stufe 2 für sn_basis, dann für die Fachplugins.
  5. Stufe 3, sobald mehr als eine Handvoll QGIS-Tests existiert.
  6. Stufe 4 als eigenes Vorhaben mit eigenem Konzept.

Die Stufen 1 bis 3 sind zusammen etwa der Aufwand, den heute ein größeres Fach-Issue kostet. Stufe 4 ist ein eigenes Vorhaben.

Quellen

Recherchestand 04.09.2026.

Vorbilder mit echter QGIS-Testsuite, als Muster zum Abschauen:

  • 3liz/lizmap-plugin — Matrix über drei QGIS-Versionen, eigener start_app()-Ersatz, dem hiesigen Setup am nächsten
  • opengisch/qfieldsync — Dialog-Tests gegen echtes QGIS, Matrix inklusive QGIS 4
  • opengisch/qgis_plugins_test_demo — kleines Referenz-Repo, unter anderem ein Drucklayout-Test