pytest läuft lokal und scheitert in der CI: woran es liegt
Der CI-Server ist kein größerer Entwicklerrechner, sondern eine andere Umgebung mit anderer Zeitzone, anderem Dateisystem, frisch installierten Paketen und weniger Speicher. Genau dort sitzen die Ursachen.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der Testlauf ist nicht das Problem, der Unterschied ist es
Der Reflex ist verständlich und führt trotzdem in die falsche Richtung: Wenn der CI-Server rot meldet und der eigene Rechner grün, muss etwas mit dem CI-Server nicht stimmen. Also wird der Lauf noch einmal gestartet, dann wird ein Zwischenspeicher geleert, und wenn es danach durchläuft, gilt die Sache als erledigt. Sie ist es nicht: Wer einen Lauf wiederholt, bis er passt, hat sich gerade angewöhnt, das Ergebnis der Testsuite zu ignorieren.
Der Grund für die Abweichung ist fast immer, dass der CI-Server ehrlicher ist. Er startet mit einem frisch geklonten Repository, installiert die Abhängigkeiten neu, kennt keine Datei, die nur bei dir auf der Platte liegt, hat keine Umgebungsvariable aus deiner Shell und läuft in einer anderen Zeitzone. Damit prüft er in Wirklichkeit etwas, das dein Rechner gar nicht prüfen kann, nämlich ob das Projekt aus dem Repository heraus funktioniert.
Teuer wird das an zwei Stellen. Wenn die Ursache nicht gefunden wird, wächst die Zahl der Tests, die als unzuverlässig gelten und irgendwann übersprungen werden, und mit ihr die Zahl der Fehler, die niemand mehr bemerkt. Und wenn dieselbe Abweichung später in der Produktion auftritt, ist es genau der Unterschied, den der CI-Server die ganze Zeit gemeldet hat, nur diesmal mit Kunden davor.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Die sechs Ursachen, die im Alltag wirklich auftreten
Die Reihenfolge folgt dem, was im Protokoll zuerst auffällt. Zu jedem Fall gehört der Unterschied zwischen den beiden Umgebungen, aus dem er folgt, und der Handgriff, der ihn dauerhaft erledigt, statt den Lauf noch einmal zu starten.
Symptom
Der Lauf bricht beim Einsammeln mit einem Importfehler ab, oder es werden deutlich weniger Tests gefunden als lokal.
Ursache
In der Voreinstellung stellt pytest das Verzeichnis der Testdatei vorn in den Suchpfad und importiert von dort. Lokal funktioniert das zusätzlich deshalb, weil dein Paket längst in der Umgebung installiert ist, oft veränderbar eingebunden aus einer früheren Sitzung. Im Build fehlt genau diese Installation, oder das Wurzelverzeichnis wird anders bestimmt, weil dort andere Dateien liegen.
Lösung
Installiere das eigene Paket im Build ausdrücklich, statt dich auf den Suchpfad zu verlassen. Leg die Tests als richtiges Paket an, also mit Dateien namens __init__.py, und stell die Importart auf importlib um, dann verändert pytest den Suchpfad überhaupt nicht mehr. Vergleich zur Kontrolle die Zeile mit dem Wurzelverzeichnis und die Zahl der eingesammelten Tests zwischen beiden Umgebungen.
Symptom
Einzelne Tests scheitern nur, wenn die ganze Suite läuft, und sind allein aufgerufen grün. Im Build tritt das häufiger auf als lokal.
Ursache
Die Tests hängen voneinander ab. Einer legt einen Datensatz an, den der nächste vorfindet, ein Modul merkt sich beim ersten Import einen Zustand, oder ein Zwischenspeicher wird zwischen den Tests nicht geleert. Lokal fällt das nicht auf, weil du meist nur einen Teil laufen lässt. Der Build verteilt die Suite dagegen auf mehrere Prozesse, und damit ist die Reihenfolge eine andere.
Lösung
Reproduzier den Fall bewusst, indem du lokal in zufälliger Reihenfolge und verteilt auf mehrere Prozesse läufst. Danach bringst du jeden Test dazu, seinen Zustand selbst herzustellen: eigene Datensätze pro Test, Datenbanktransaktionen, die am Ende zurückgerollt werden, und Vorrichtungen mit passendem Gültigkeitsbereich statt globaler Variablen. Ein Zustand, den mehrere Tests teilen, gehört ausdrücklich in eine Fixture und nicht in ein Modul.
Symptom
Ein Test mit Datum oder Uhrzeit scheitert im Build, oder er scheitert nur zu bestimmten Tageszeiten und am Monatsende.
Ursache
CI-Server laufen fast immer in UTC, dein Rechner in der lokalen Zeitzone. Damit fällt ein Datumswechsel an einer anderen Stelle, und eine Berechnung über Tagesgrenzen kippt. Dazu kommt die Umstellung auf Sommerzeit, bei der eine Stunde doppelt vorkommt und eine fehlt. Ähnlich wirkt die Spracheinstellung: Sie verändert Dezimaltrennzeichen, Monatsnamen und die Sortierreihenfolge von Zeichenketten.
Lösung
Rechne intern mit Zeitangaben, die ihre Zeitzone kennen, und bilde lokale Zeiten mit zoneinfo ausdrücklich ab, statt sie vom System zu erben. Für Tests, die von einem festen Zeitpunkt ausgehen, setz die Zeit über eine Vorrichtung, statt die aktuelle Uhrzeit zu nehmen. Setz Zeitzone und Spracheinstellung im Build und in deiner lokalen Vorlage auf denselben Wert, dann verschwindet die ganze Klasse.
Symptom
Der Build schlägt ohne Codeänderung fehl, oft mit einem Fehler in einer Bibliothek, die niemand angefasst hat, oder mit einer Warnung, die zum Fehler erklärt wird.
Ursache
Im Build wird ohne feste Fassungen installiert und deshalb jedes Mal neu aufgelöst. Deine lokale Umgebung ist dagegen Monate alt und enthält die Fassungen von damals. Sobald irgendwo im Abhängigkeitsbaum eine neue Version erscheint, prüft der Build gegen andere Bibliotheken als du. Wenn in der Konfiguration Warnungen als Fehler behandelt werden, reicht dafür schon eine neue Deprecation-Warnung.
Lösung
Führ ein Lockfile ein und installiere im Build ausschließlich daraus. Aktualisierungen werden damit zu einer eigenen Änderung mit eigenem Testlauf, statt sich unbemerkt einzuschleichen. Wenn Warnungen als Fehler gelten sollen, dann bewusst und mit gepflegter Ausnahmeliste, sonst legt eine fremde Bibliothek euren Build lahm.
Symptom
Ein Test findet eine Datei nicht, oder er findet sie, arbeitet aber mit einem anderen Inhalt als erwartet.
Ursache
Dafür gibt es drei verbreitete Gründe. Die Datei liegt bei dir auf der Platte, ist aber nie ins Repository gelangt, etwa weil ein Muster in der .gitignore sie erfasst. Der Test öffnet sie über einen Pfad relativ zum Arbeitsverzeichnis, und das ist im Build ein anderes. Oder das Dateisystem unterscheidet sich: Auf macOS und Windows ist die Groß- und Kleinschreibung meist egal, auf dem Linux-Runner nicht, und die Reihenfolge beim Auflisten eines Verzeichnisses ist ohnehin nirgends zugesichert.
Lösung
Bau Pfade zu Testdaten immer relativ zur Testdatei selbst auf und nie relativ zum Arbeitsverzeichnis. Schreibende Tests arbeiten in einem temporären Verzeichnis, das pytest je Test bereitstellt, statt neben den Quellen. Sortiere jede Liste von Dateinamen ausdrücklich, bevor du dich auf ihre Reihenfolge verlässt, und prüf im Zweifel mit einem frischen Klon in einem leeren Verzeichnis, ob wirklich alles vorhanden ist.
Symptom
Der Lauf bricht ohne verwertbare Meldung ab, läuft in einen Timeout oder endet mit einem Exit-Code, den kein Test erzeugt hat.
Ursache
Der Runner hat weniger Arbeitsspeicher und weniger Kerne, als der Rechner unter deinem Schreibtisch. Ein Test, der eine große Datei einliest oder eine Datenbank im Speicher füllt, wird dort vom Betriebssystem abgeräumt. Dazu kommen fehlender Netzzugang, nicht gesetzte Zugangsdaten, ein Dienst, der noch nicht bereit ist, wenn die Tests starten, und Wartezeiten, die auf einem geteilten Runner schlicht nicht reichen.
Lösung
Sieh dir zuerst den Abbruchcode und den Speicherverbrauch an, statt im Testcode zu suchen. Lass dir die langsamsten Tests ausgeben und begrenze den Speicherbedarf einzelner Fälle. Ersetz feste Wartezeiten durch aktives Warten auf eine Bereitschaftsprüfung. Tests, die tatsächlich ins Netz greifen, gehören entweder in eine eigene, ausdrücklich gekennzeichnete Gruppe oder hinter eine Attrappe.
Fünf Prüfungen in der Reihenfolge, in der sie sich lohnen
- 01 Stimmen Interpreter, Paketfassungen und Wurzelverzeichnis mit deinem Rechner überein?
- 02 Sammelt der Lauf dieselbe Anzahl Tests ein wie bei dir?
- 03 Bleibt die Suite grün, wenn du sie zufällig sortiert und verteilt laufen lässt?
- 04 Läuft sie in der Zeitzone und der Spracheinstellung des CI-Servers noch durch?
- 05 Existiert jede Datei, die der Test öffnet, auch im frisch geklonten Repository?
Was du danach in einer halben Stunde eingrenzt
Die Suche folgt einer festen Reihenfolge, von der billigsten Prüfung zur teuersten. Sechs Schritte reichen für fast jeden Fall, und der erste davon spart dir häufig alle weiteren.
Die Umgebung im Lauf sichtbar machen
Lass den Build vor den Tests ausgeben, welcher Interpreter benutzt wird, welche Pakete in welchen Fassungen installiert sind, wie die Zeitzone eingestellt ist und in welchem Verzeichnis gearbeitet wird. Diese wenigen Zeilen im Protokoll beantworten mehr Fragen als jede Vermutung und kosten nichts.
Den Kopfbereich der pytest-Ausgabe lesen
pytest gibt beim Start rootdir, die benutzte Konfigurationsdatei und die geladenen Zusatzmodule aus. Wenn dort ein anderes Wurzelverzeichnis steht als lokal oder ein Zusatzmodul fehlt, ist die Ursache damit schon gefunden. Die Zahl der eingesammelten Tests gehört zum selben Vergleich.
Lokal unter denselben Bedingungen laufen lassen
Setz die Zeitzone auf die des CI-Servers, arbeite in einer frisch angelegten Umgebung und benutz dasselbe Image, wenn es eines gibt. Ein Lauf im Container mit dem Repository in einem frischen Verzeichnis reproduziert die meisten Fälle innerhalb weniger Minuten.
Reihenfolge und Parallelität ausdrücklich prüfen
Lauf die Suite einmal in zufälliger Reihenfolge und einmal verteilt auf mehrere Prozesse. Wenn sie dabei lokal rot wird, hast du keinen Umgebungsfehler, sondern Tests, die voneinander abhängen, und das ist eine andere Baustelle mit anderer Lösung.
Die Abhängigkeiten festnageln
Solange im Build ohne Lockfile installiert wird, ist jeder Lauf eine neue Zusammenstellung. Ein Lockfile mit allen indirekten Abhängigkeiten macht aus einem beweglichen Ziel einen festen Stand, und Aktualisierungen werden dann zu einer eigenen, sichtbaren Änderung.
Fehlgeschlagene Läufe auswertbar machen
Lass Berichte, Protokolle und erzeugte Dateien als Artefakte des Laufs ablegen und aktivier die ausführliche Ausgabe für lokale Variablen. Ohne das rätselst du an einer Meldung herum, die auf deinem Rechner gar nicht entsteht, und beginnst wieder mit dem Wiederholen des Laufs.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der Importpfad ist der häufigste Einzelfall
pytest kennt drei Arten, Testmodule zu importieren. In der Voreinstellung wird das Verzeichnis der Testdatei vorn in den Suchpfad gestellt. Das stammt aus einer Zeit, in der das der einzige gangbare Weg war, und es hat eine unangenehme Folge: Zwei Testdateien mit demselben Namen in verschiedenen Verzeichnissen kollidieren, sobald die Verzeichnisse keine Pakete sind. Die Dokumentation empfiehlt deshalb ausdrücklich, Testverzeichnisse als Pakete anzulegen.
Die dritte Art benutzt die Importmaschinerie direkt und rührt den Suchpfad überhaupt nicht an. Damit verschwindet die gesamte Klasse an Überraschungen, in der lokal etwas gefunden wird, das im Build fehlt. Der Preis ist, dass Testmodule sich untereinander nicht mehr importieren können, was in einer sauber geschnittenen Suite ohnehin selten nötig ist und sich sonst über gemeinsame Vorrichtungen lösen lässt.
Der zweite Teil desselben Themas ist die Frage, woher das eigene Paket kommt. Wenn es lokal irgendwann veränderbar installiert wurde und im Build nicht, testest du an zwei verschiedenen Ständen: einmal gegen den Quelltext im Arbeitsverzeichnis, einmal gegen ein gebautes Paket. Eine Verzeichnisstruktur mit einem eigenen Quellverzeichnis erzwingt hier Klarheit, weil der Import ohne Installation gar nicht erst funktioniert und der Fehler damit sofort und überall auftritt statt nur an einer Stelle.
Alles, was von außen kommt: Zeit, Sprache, Zufall, Reihenfolge
Zeit ist der Klassiker. CI-Server laufen praktisch immer in UTC, Entwicklerrechner in der lokalen Zeitzone, und damit fällt der Tageswechsel an unterschiedlichen Stellen. Ein Test, der prüft, ob ein Vorgang von gestern richtig einsortiert wird, kann deshalb zwischen zwei und drei Uhr morgens rot werden und danach wieder grün. Dazu kommt die Sommerzeitumstellung mit einer doppelten und einer fehlenden Stunde, die jede Rechnung mit einer festen Zahl von Sekunden am Tag aushebelt.
Die Spracheinstellung wirkt leiser und ist deshalb schwerer zu finden. Sie bestimmt, ob eine Zahl mit Komma oder Punkt formatiert wird, wie Monatsnamen aussehen und in welcher Reihenfolge Zeichenketten sortiert werden. Ein Test, der eine sortierte Liste mit Umlauten vergleicht, kann allein daran scheitern, dass der Runner eine andere Einstellung mitbringt.
Bleibt der Zufall. Python würfelt seit Langem den Startwert für das Hashing von Zeichenketten bei jedem Start neu, weshalb die Reihenfolge, in der Elemente einer Menge herauskommen, zwischen zwei Läufen abweichen darf. Wer diese Reihenfolge in einer Zusicherung festhält, hat einen Test gebaut, der irgendwann scheitert, und der CI-Server findet das nur früher, weil er häufiger läuft. Sortier vor dem Vergleich, oder vergleich Mengen statt Listen.
Der CI-Server ist kein größerer Entwicklerrechner
Ein Runner ist meist knapper ausgestattet, als es die Erfahrung am eigenen Arbeitsplatz vermuten lässt, und er wird häufig geteilt. Ein Test, der eine große Datei vollständig einliest, läuft lokal in Sekunden und wird dort vom Betriebssystem abgeräumt. Erschwerend kommt hinzu, dass die Zahl der sichtbaren Kerne in einem Container nicht mit der tatsächlich zugeteilten Rechenzeit übereinstimmen muss, weshalb eine automatische Aufteilung auf alle Kerne die Sache verschlimmern kann statt sie zu beschleunigen.
Der zweite Unterschied ist die Umgebung drumherum. Auf deinem Rechner liegt eine Datei mit Konfigurationswerten, die nie ins Repository gehört, und deine Shell hat seit Monaten Variablen gesetzt, an die niemand mehr denkt. Im Build ist beides nicht da, und dort steht stattdessen eine Secrets-Verwaltung, die bei einem Pull Request aus einem Fork des Repositorys aus gutem Grund nichts herausgibt.
Der dritte Unterschied betrifft Dienste. Datenbank, Nachrichtenwarteschlange und Zwischenspeicher laufen bei dir seit Wochen, im Build starten sie zusammen mit dem Job. Ein Dienst, der einen Netzwerkport bereits geöffnet hat, ist aber noch längst nicht bereit, Anfragen zu beantworten. Feste Wartezeiten verdecken das nur so lange, bis der Runner einmal stärker ausgelastet ist, weshalb an diese Stelle eine echte Bereitschaftsprüfung gehört.
Wann es kein Umgebungsproblem ist
Es gibt einen Fall, in dem die ganze Suche in die falsche Richtung geht: wenn derselbe Lauf auch lokal manchmal rot ist und nur seltener. Dann liegt keine Abweichung zwischen zwei Umgebungen vor, sondern ein Test, dessen Ergebnis von Bedingungen abhängt, die er nicht festlegt. Der CI-Server macht das nur sichtbarer, weil er die Suite häufiger, verteilter und unter anderer Last ausführt.
Die Unterscheidung ist billig zu treffen. Lass die Suite lokal zwanzigmal hintereinander in zufälliger Reihenfolge laufen. Bleibt sie durchgehend grün, ist es tatsächlich die Umgebung, und die Liste oben führt zum Ziel. Wird sie zwischendurch rot, hast du ein anderes Problem und brauchst eine andere Vorgehensweise, nämlich das Festhalten von Zeit, Reihenfolge und gemeinsam benutztem Zustand im Test selbst.
Was in beiden Fällen nicht hilft, ist das automatische Wiederholen fehlgeschlagener Tests. Es gibt Situationen, in denen ein Wiederholungsversuch vertretbar ist, etwa bei einer echten Netzabhängigkeit, die ihr nicht kontrolliert. Als Standardeinstellung sorgt es dafür, dass ein Test, der in einem von zehn Läufen einen echten Fehler findet, nie wieder auffällt.
Dazu passende Kurse
Wenn die Suite regelmäßig rot ist, ohne dass jemand etwas geändert hat, setzen Kurse zu verlässlichen Testläufen genau an dieser Stelle an.
Weil ein Teil der Ursachen in Runnern, Images und Diensten steckt und nicht im Testcode, gehören Kurse rund um CI-Umgebungen und Auslieferung zur selben Frage.
Wie sicher bist du beim Thema wirklich?
Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.
Wo du genau das übst
Sehr umfangreicher Überblick über Zusammenhänge und Anwendungsbereiche. Sehr zu empfehlen.
Effektives, gemeinsames Erarbeiten der Aufgaben mit vielen praktischen Tipps.
Sehr guter Trainer und gute Aufbereitung der Inhalte und auch eingehen auf Fragen der Beteiligten.
Häufige Fragen
Sollen wir den Testlauf im Build einfach wiederholen lassen?
Warum findet der Build unser eigenes Paket nicht?
Wie testen wir Code, der die aktuelle Uhrzeit benutzt?
Lohnt es sich, die Tests im Build zu verteilen?
Sollen Warnungen im Build als Fehler behandelt werden?
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Programmierung-Programm den passenden Kurs für deinen Stand zu finden.
Norbert Jansen
Beratung & Inhouse
Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.
Tests schreiben, die überall dasselbe Ergebnis liefern
Im pytest-Kurs bei cmt baust du Fixtures, temporäre Verzeichnisse und Mocks so auf, dass ein Testlauf von Zeitzone, Reihenfolge und Rechner unabhängig wird. Die Kurse laufen vor Ort und Live-Online.