29.000 Tests — wie ich sie organisiere und schnell halte
22.09.2026 · technik
Im Beitrag 29.000 Tests & Clean Architecture ging es darum, warum ich so viel teste. Hier geht es darum, wie.
Das wird technisch. Wenn Sie wissen wollen, was davon bei Ihnen im Betrieb ankommt, sind Sie mit dem anderen Beitrag besser bedient.
Kein Haufen, sondern Ebenen
Zehntausende Tests in einem einzigen Projekt wären nicht zu bedienen. Bei bitERP verteilen sie sich auf 29 Testprojekte, geschnitten entlang der Architektur:
- Domain — die Fachregeln pur. Keine Datenbank, kein Netzwerk. Läuft in Millisekunden.
- Application — die Anwendungsfälle, mit einem eigenen Projekt je Modul: Sales, Inventory, Finance, Production, Procurement, Logistics, CRM, Compliance und weitere.
- Infrastructure und API — alles, was nach außen spricht: Datenbank, Endpunkte, Schnittstellen.
- Integration und Web — die Bausteine im Zusammenspiel.
- E2E — der komplette Durchlauf. Einmal als reiner Prozess, einmal durch die echte Oberfläche im Browser.
Dazu kommen die nativen Stationen: Packtisch und Produktions-Workbench haben eigene Testprojekte.
Diese Aufteilung ist kein Ordnungsfimmel. Sie ist die Voraussetzung dafür, dass die Tests überhaupt schnell bleiben können.
Warum der Zuschnitt die Laufzeit rettet
Die Ebenen unterscheiden sich um Größenordnungen. Ein Domain-Test rechnet eine Regel nach. Ein UI-Test startet einen Browser.
Wie sehr, zeigt ein Blick in einen echten Durchlauf. Die 16 Modul-Projekte bringen 16.404 Tests mit und sind nach gut elf Minuten durch. Die anderen acht — Domain, Infrastructure, Integration, Web und die Stationen — kommen auf 8.922 Tests und brauchen fast vierzehn.
Halb so viele Tests, mehr Zeit. Der Grund ist simpel: In dieser Hälfte sitzt alles, was eine Datenbank anfasst. Nicht die Anzahl entscheidet über die Laufzeit, sondern was ein einzelner Test kostet.
Genau deshalb laufen die beiden Hälften nebeneinander statt hintereinander, und innerhalb jeder Hälfte wieder zwei Testmodule gleichzeitig.
Die Aufteilung steckt in zwei Solution-Filtern, nicht in einem Namensfilter über die Gesamtlösung. Das klingt nach einer Kleinigkeit, ist aber der Unterschied zwischen grün und rot: Ein Namensfilter, der auf ein Projekt trifft, in dem gerade kein Test übrig bleibt, färbt den Job rot. Bei einem Solution-Filter lädt jede Hälfte nur die Projekte, die sie ohnehin ausführt — jedes geladene Modul hat garantiert Treffer.
Die API-Tests bekommen einen eigenen Job und stehen bewusst in keinem der beiden Filter. Sonst liefen sie doppelt.
Der schwierige Teil sind die UI-Tests
Hier wird es interessant, weil echte Nebenläufigkeit ins Spiel kommt.
Die UI-Tests fahren einen echten Browser gegen eine echte PostgreSQL. Rund 1.900 davon prüfen Anlegen, Ändern und Löschen quer durch die Masken — dieselbe Mechanik, immer wieder, für jede Entität.
Zusammen sind das 2.352 Tests, aufgeteilt auf vier Shards, die parallel laufen — geschnitten nach Fachbereich:
- A — Prozesse, Katalog und Bestand: 589 Tests, gut fünf Minuten
- B — Prozesse, Navigation, Berechtigungen, Grid-Bedienung: 512 Tests, gut neun Minuten
- C — Seiteneffekte und Finanzen: 548 Tests, neun Minuten
- D — Logistik und Seriennummern: 703 Tests, gut zehn Minuten
Die Zuordnung hängt an einem Trait, den die CRUD-Tests über ihre Basisklasse erben. Ein neuer Test landet damit automatisch im richtigen Shard, ohne dass jemand eine Liste pflegt.
Perfekt ausbalanciert ist das nicht — zwischen dem kleinsten und dem größten Shard liegen knapp 200 Tests. Interessanter ist aber, dass die Laufzeiten trotzdem enger beieinander liegen als die Testzahlen. Auch hier gilt: Was ein Test kostet, sagt mehr als wie viele es sind.
Und innerhalb eines Shards laufen mehrere Test-Collections gleichzeitig. Damit sie sich nicht gegenseitig die Daten umgraben, legt sich jede ihre eigene Datenbank an.
Das ist die Sorte Fehler, die man nicht kommen sieht: Migrieren genug Collections gleichzeitig ihr Schema, sprengt PostgreSQL mit den Standardeinstellungen seine interne Lock-Tabelle. Der Lauf bricht ab mit „out of shared memory“ — und sieht aus wie ein Fehler im Code. Die Lösung war am Ende eine einzige hochgesetzte Konfigurationszeile.
Was bei jeder Änderung läuft
Die Pipeline hat fünf Stufen: Images, Build, Test, Desktop-Pakete, Paketierung. Geprüft wird dabei nicht nur der Code.
Zusammengerechnet stecken in einem grünen Lauf fast 69 Minuten reine Testzeit. So lange wartet aber niemand: Die Jobs laufen nebeneinander, und der längste einzelne braucht knapp vierzehn Minuten. Genau dafür ist der ganze Zuschnitt da.
Und wenn ich zweimal kurz hintereinander etwas hochlade, bricht die alte Pipeline ab, statt Rechenzeit für einen längst überholten Stand zu verbrennen.
Ein eigener Job nimmt eine leere Datenbank und lässt die komplette Migrationskette von null durchlaufen. Das klingt nach wenig, schließt aber eine Lücke, die sonst keiner sieht: Alle übrigen Tests bauen ihr Schema direkt aus dem Datenmodell auf, nie über die Migrationen. Eine Migration, die eine Spalte anlegt, die es laut erster Migration längst gibt, fiele in keinem einzigen anderen Test auf — sondern erst dann, wenn zum ersten Mal jemand eine neue Datenbank aufsetzt.
Was dieser Job ausdrücklich nicht beweist: dass ein Update auf einer gewachsenen Datenbank sauber durchläuft. Migrationen, die vorhandene Daten umbauen, eine Pflichtspalte in einer bereits gefüllten Tabelle, eine Sperre auf einer Tabelle mit Millionen Zeilen — das ist ein eigenes Problem, und dafür braucht es einen echten Bestand zum Prüfen.
Die Kette ist also abgesichert. Der Weg über gewachsene Daten noch nicht. Das gehört zu den Dingen, die ich lieber benenne, als sie hinter einer Zahl verschwinden zu lassen.
Was das im Alltag bedeutet
Ich kann eine Änderung machen und bekomme eine belastbare Antwort, ohne mich irgendwo von Hand durchzuklicken.
Das ist der eigentliche Grund für den Aufwand. Nicht die Zahl der Tests. Sondern dass ich mich traue, etwas anzufassen.