# Plan: automatisierte Tests gegen echtes QGIS Stand 04.09.2026. Vorschlag zur Abstimmung im Team. Ergänzt [`../leitfaden/06_tests.md`](../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. ```python 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 ```mermaid flowchart LR S1["Stufe 1
start_app() lokal
klein"] S2["Stufe 2
Gitea Actions
Matrix 3.44 / 4.2
mittel"] S3["Stufe 3
pytest-qgis
klein"] S4["Stufe 4
Render-Vergleich
Plan41
groß"] 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`. ```yaml 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. - `qgis.testing` (`start_app`, `QgisTestCase`): https://github.com/qgis/QGIS/blob/master/python/testing/__init__.py - `qgis.testing.mocked.get_iface()`: https://github.com/qgis/QGIS/blob/master/python/testing/mocked.py - Docker-Images und Test-Helfer: https://github.com/qgis/QGIS/blob/master/.docker/README.md - Tag-Liste: https://hub.docker.com/r/qgis/qgis/tags - Release-Fahrplan (LTR-Termine): https://qgis.org/resources/roadmap/ - Unit-Testing und Render-Checks: https://docs.qgis.org/testing/en/docs/developers_guide/unittesting.html - `QgsRenderChecker` vs. `QgsMultiRenderChecker`: https://github.com/qgis/QGIS/blob/master/src/core/qgsrenderchecker.h - `pytest-qgis`: https://github.com/GispoCoding/pytest-qgis · https://pypi.org/project/pytest-qgis/ - Modale Dialoge im Test: https://pytest-qt.readthedocs.io/en/latest/note_dialogs.html - Migration zu Qt6/QGIS 4: https://plugins.qgis.org/docs/migrate-qgis4/ · https://github.com/qgis/pyqgis4-checker - Gitea Actions, Runner-Labels: https://docs.gitea.com/usage/actions/act-runner/ 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