Testauswahl in der CI-Pipeline bedeutet, bei jedem Commit automatisch nur jene Tests auszuführen, die den geänderten Sourcecode tatsächlich abdecken. Das Werkzeug dafür analysiert Code-Coverage auf Einzeltestebene und priorisiert die Auswahl so, dass die Ausführungszeit unter fünf Minuten bleibt. Das Ziel ist eine Gesamtlaufzeit der Pipeline von maximal zehn Minuten bei rund 6.000 Testfällen.
Das Wichtigste in Kürze
- Die CI-Pipeline bei Dolby ist hart auf 10 Minuten begrenzt, wovon maximal 5 Minuten auf die Testausführung entfallen, der Rest auf Build, Umgebung und Unit-Tests.
- TeamScale priorisiert die zurückgegebenen Tests automatisch nach Nähe zum geänderten Source Code, Fehlerhäufigkeit und Aktualität, sodass die relevantesten Tests zuerst ausgeführt werden.
- PyTest-Parametrisierung erzeugt aus einer einzigen Testfunktion schnell 50 bis 100 Testfälle, weshalb bei 6.000 Testfällen nicht alle in die CI passen und eine zeitbasierte Selektion nötig wird.
- Die Integration von PyTest und TeamScale dauerte zwei bis drei Wochen, weil für Python zunächst keine native Unterstützung existierte und ein eigenes Plugin für die testgenaue Code-Coverage-Erfassung geschrieben werden musste.
- Der Code-Coverage-Upload läuft nächtlich, sodass TeamScale maximal einen Tag hinter dem aktuellen Stand liegt und bei Bedarf manuell für einzelne Branches aktualisiert werden kann.
Warum schnelle CI-Pipelines bei wachsenden Testmengen scheitern
Eine CI-Pipeline soll in Minuten Rückmeldung geben, nicht in Stunden. Genau das wird schwer, sobald die Zahl der Testfälle wächst. Viele Teams kennen das Endstadium: 6.000 Tests, eine Laufzeit über Nacht, und tagsüber bleibt nur die Hoffnung, dass schon das Wichtigste mitläuft.
Lars Kempe, QA Lead bei Dolby, beschreibt den Kern des Problems anhand der eigenen Testlandschaft. Das Team automatisiert zu 100 Prozent mit PyTest und baut Libraries, die an Kunden ausgeliefert werden. Die volle Suite mit rund 6.000 Testfällen läuft nachts unter Linux etwa anderthalb Stunden. Für eine CI ist das zu lang.
Der eigentliche Treiber der Testmenge ist die Parametrisierung. Aus einer einzelnen Testfunktion werden über verschiedene Konfigurationen schnell viele Testfälle. Bei Audio reicht der Bogen von Mono über Stereo und 5.1 bis zu Dolby Atmos mit zusätzlichen Höhenkanälen. Kombiniert man das mit verschiedenen Bitraten und Frame-Raten, entstehen aus einem Test schnell 50 bis 100 Test-Cases.
Manuelle Testselektion verfällt, sobald keiner sie pflegt
Wer Tests über Marker von Hand auswählt, muss diese Auswahl ständig nachziehen. Das ist die unbequeme Wahrheit hinter jeder schnellen Pipeline, die ohne Werkzeug auskommen will.
Die Pflege fällt in der Praxis hinten runter. Man müsste sich alle zwei Wochen hinsetzen und die Marker an den neu hinzugekommenen Code anpassen. Stattdessen läuft eine Basisauswahl, von der niemand sicher weiß, ob sie die neuen Änderungen wirklich abdeckt.
Das führt zu einer trügerischen Sicherheit. Die Pipeline ist grün, weil “wenigstens das Wichtigste” läuft. Ob der frisch geänderte Code dabei ist, bleibt offen. Genau an diesem Punkt entschied sich das Team gegen die manuelle Pflege und für eine werkzeuggestützte Auswahl.
Wie eine testbasierte Auswahl mit TeamScale funktioniert
Der Ansatz steht und fällt mit einer Information: Welcher Test deckt welchen Source Code ab? TeamScale braucht diese Verbindung, um bei einer Code-Änderung gezielt die passenden Tests auszuwählen.
Die Grundlagen sind schnell eingerichtet. Die Git-Integration ist in etwa zehn Minuten erledigt. Danach kennt TeamScale die Branches, die Commits und die Code-Basis. Der aufwendige Teil ist die Verknüpfung zwischen Tests und abgedecktem Code.
Diese Verknüpfung entsteht über Code Coverage auf Ebene des einzelnen Tests. Das Team instrumentiert die Binaries mit GCOV und misst die Abdeckung. Das Problem an der Gesamt-Coverage: Sie zeigt nicht, welcher einzelne Test welche Zeilen berührt hat. Manuell müsste man jeden Test einzeln ausführen, die Coverage wegschreiben, alles zurücksetzen und den nächsten Test starten.
Das PyTest-Plugin als Brücke zur Coverage
Die Lösung war ein eigenes PyTest-Plugin, das in die Hooks von PyTest eingreift. Nach jeder einzelnen Testausführung, nicht nur nach jeder Testfunktion, wird ein Hook getriggert. An diesem Punkt liefert PyTest bereits den Testnamen, den Status (Pass, Fail, Skip) und die Laufzeit.
Die fehlende Information ist der abgedeckte Code. Dafür wertet das Plugin die Logfiles von GCOV aus und liest die abgedeckten Dateien samt Zeilennummern heraus. Diese Daten landen in einem Logfile in dem Format, das TeamScale erwartet, und werden hochgeladen.
Der Aufwand für diese feingranulare Integration lag bei zwei bis drei Wochen. TeamScale war anfangs stark auf Java fokussiert und unterstützte die PyTest-Umgebung nicht direkt. Die Anbindung lief deshalb über die API, in enger Abstimmung mit dem Anbieter.
Die Datenbasis wird nächtlich aktualisiert, der Code laufend
Source Code und neue Tests kommen automatisch in TeamScale an, die Coverage-Daten dagegen werden einmal pro Nacht aktualisiert. Diese Trennung ist eine bewusste Entscheidung mit einer klaren Konsequenz.
Über die Git-Integration weiß TeamScale jederzeit, welcher Code und welche Tests existieren. Die Zuordnung Test-zu-Code dagegen entsteht erst beim nächtlichen Lauf. Daraus ergibt sich ein Versatz von maximal einem Tag: Ein neuer Test, der tagsüber parallel zum Code entsteht, ist erst am Folgetag in der Auswahl berücksichtigt.
Für längere oder größere Branches lässt sich die Aktualisierung manuell anstoßen. Ein Update auf den Branch genügt, danach passt die Zuordnung wieder. Im Alltag bleibt der eintägige Versatz unkritisch, weil der entscheidende Mechanismus automatisch greift: Ändert jemand Code, werden am nächsten Tag genau die zugehörigen Tests ausgewählt.
Die Zeit ist das harte Auswahlkriterium, nicht die Testmenge
Die Pipeline wählt Tests nicht nach Anzahl, sondern nach verfügbarer Zeit aus. Das Limit liegt bei fünf Minuten reiner Testausführungszeit, eingebettet in eine Gesamt-CI von zehn Minuten.
Bei einer Code-Änderung liefert TeamScale die relevanten Tests bereits priorisiert zurück. In die Priorisierung fließen ein, wie nah ein Test am geänderten Code liegt, wie oft er zuvor fehlgeschlagen ist und wie neu er ist. Eine einzelne Änderung kann durch die Parametrisierung trotzdem 800 Tests zurückgeben.
Diese 800 Tests will niemand alle ausführen. Anfangs summierte das Team die Laufzeiten der priorisierten Tests auf und schnitt nach fünf Minuten ab. Später kam auf Anfrage eine API-Funktion dazu, der man die verfügbare Zeit direkt mitgibt. TeamScale liefert dann genau die Tests zurück, die in dieses Zeitfenster passen.
Die Zeitgrenze schützt vor dem Rückfall in den Ausgangszustand. Ohne sie könnte eine umfangreiche Änderung im schlechtesten Fall wieder Tausende Tests in die Pipeline ziehen.
So sieht der Ablauf in der Pipeline aus
Build und Testvorbereitung laufen parallel, das spart die wertvollen Minuten. Während der Build läuft, werden das Testenvironment installiert und die Tests von TeamScale abgefragt.
Die Abfrage selbst ist eine API-Anfrage über die Commit-ID. Eine kleine Hürde: In der genutzten API-Version erwartet TeamScale nicht den Namen der Commit-ID, sondern deren Zeitstempel. Die Umrechnung erledigt ein Git-Befehl als Einzeiler.
TeamScale antwortet mit einem JSON-File, das sich direkt in ein Python-Dictionary umwandeln lässt und die Testlaufzeiten enthält. Aus den ausgewählten Test-Cases erzeugt das Team über ein PyTest-Plugin eine Textdatei, die per Kommandozeile übergeben wird. PyTest führt dann genau diese Tests aus, parallelisiert über entsprechende Plugins.
Ist der Build fertig, laufen die Tests in maximal fünf Minuten. Mit dem Rüstzeug drumherum landet die gesamte CI bei rund zehn Minuten. Schlägt sie fehl, sieht der auslösende Entwickler den Status in GitLab und bekommt eine Mail. Bei einem Team von fünf bis sechs Leuten ist die Ursache meist schnell gefunden, sonst schaut man gemeinsam hinein.
Ein kleines Team kann sein Werkzeug formen statt es nur zu bedienen
Dass die Integration in wenigen Wochen stand, hat mit der Größe des Teams zu tun. Wer Architektur und Implementierung in einer Hand hält, kommt schneller voran.
Lars beschreibt seine Doppelrolle als QA Lead, der zugleich programmiert und die QA-Tätigkeit übernimmt. In großen Organisationen trennt man Testarchitektur und Umsetzung oft in eigene Rollen, was eigene Vorteile hat. Für die schnelle Anbindung war die Nähe zum System hilfreich: Manchmal reichten zwei Stunden Pair Programming.
Hilfreich war außerdem die offene Werkzeugkette. PyTest und Python bringen viele Plugins mit, und für das Generieren der Test-Liste aus einer Textdatei gab es bereits eine passende Lösung. Wo TeamScale die PyTest-Welt nicht abdeckte, ließ sich über die API nachrüsten, teils direkt durch neue Funktionen des Anbieters.
Wenn ich das mache, dann möchte ich es ordentlich machen. Wir fangen mit einer schnellen CI von Anfang an an, auch wenn wir noch nicht so viele Tests oder Code haben. — Lars Kempe
Was als Nächstes auf der Liste steht
Die CI gilt als gelöst und läuft seit rund anderthalb Jahren stabil ohne große Änderungen. Der nächste Hebel liegt bei den Merge-Requests, die mehr Tests durchlaufen lassen sollen.
Parallel will das Team TeamScale in weitere Projekte ausrollen, die das Werkzeug bisher nicht nutzen. Das Ziel dort ist dasselbe wie im Ausgangsprojekt: die Testauswahl optimieren und die Rückmeldung schnell halten.
Testdaten sind in diesem Umfeld kein Engpass. Die Input-Files liegen vor und werden vom Test live geholt, in rund 80 Prozent der Fälle handelt es sich um Audio. Worauf man achten muss, ist die Länge: Ein PCM-File von vier Minuten zu dekodieren dauert eben auch etwa vier Minuten. Bei einem Fünf-Minuten-Budget zählt jede solche Laufzeit.


