245 lines
13 KiB
Markdown
245 lines
13 KiB
Markdown
# 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["<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`.
|
|||
|
|
|
|||
|
|
```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
|