Zum Inhalt springen

Warum Ihre Testsuite elf Minuten braucht

Eine langsame Suite ist eine Suite, die übersprungen wird. Die Zeit steckt fast nie in den Assertions, sondern im Neuaufbau der Datenbank, im Hashen von Passwörtern und in Netzaufrufen.

4 Min. Lesezeit

Eine Suite, die elf Minuten braucht, wird vor dem Push nicht ausgeführt. Sie wird von der CI ausgeführt, nachdem der Kontext verloren ist, was aus einer Korrektur von fünf Sekunden einen Rundgang von zwanzig Minuten macht. Irgendwann pusht jemand über einen roten Build hinweg, weil er ziemlich sicher ist, dass es nichts damit zu tun hat.

Die Geschwindigkeit der Suite ist also kein Komfort für Entwickler. Sie entscheidet, ob die Tests ihre Arbeit tun.

Messen statt raten

php artisan test --profile

Das gibt die langsamsten Tests aus. Fast immer macht eine Handvoll Fälle den größten Teil der Uhrzeit aus, und die Behebung ist konkret statt architektonisch.

Die Datenbank, meist der größte Teil

Für jeden Test migrieren. RefreshDatabase hüllt jeden Test in eine Transaktion und rollt sie zurück, was schnell ist. DatabaseMigrations führt jede Migration für jeden Test aus, was es nicht ist. In einem Projekt mit zweihundert Migrationen ist dieser Unterschied das ganze Problem.

Migrationen statt eines Schema-Dumps. Selbst einmal pro Durchlauf dauert das Abspielen von zweihundert Migrationen länger, als ihr Ergebnis zu laden:

php artisan schema:dump --prune

Die Suite lädt dann eine SQL-Datei. Das ist der größte Einzelgewinn in einer gewachsenen Codebasis und kostet einen Befehl.

Factories, die mehr anlegen als der Test braucht. Eine Factory mit einer has()-Kette, die zwanzig verbundene Datensätze erzeugt, für einen Test, der ein Feld liest. Legen Sie das Minimum an, und verwenden Sie make() statt create(), wo nichts die Datenbank berührt.

Passwort-Hashing, womit niemand rechnet

Hashing ist absichtlich langsam – das ist sein Zweck. Eine Suite, die über eine Factory hunderte Nutzer anlegt, zahlt diese Kosten hundertfach.

// tests/TestCase.php oder Pest.php
Hash::driver('bcrypt')->setRounds(4);

Bei Suiten mit vielen Nutzer-Fixtures hat das allein Laufzeiten halbiert.

Tests, die ins Netz greifen

Die schädlichste Kategorie, weil sie langsam und unzuverlässig zugleich ist. Ein Test, der eine echte API aufruft, wartet auf den Server eines anderen und scheitert, wenn der einen schlechten Tag hat.

Http::preventStrayRequests();

Das gehört in die Basisklasse der Tests. Jeder Test, der einen ungefälschten HTTP-Aufruf macht, scheitert nun sofort und sagt Ihnen welcher – und Sie werden welche finden, von denen Sie nichts wussten.

Dasselbe gilt für Queues, Mail und Storage. Gefälscht sind sie sofort da; echt machen sie Arbeit, über die der Test gar nichts behauptet.

Warten und Pollen

sleep(2) in einem Test, der auf etwas Asynchrones wartet, sind zwei Sekunden bei jedem Durchlauf, für immer, und es ist entweder zu lang oder – auf einer langsameren Maschine – nicht lang genug.

Laravels Zeit-Helfer machen Warten überflüssig: Zeit einfrieren, vorspulen, prüfen. Für wirklich asynchrone Arbeit führen Sie die Queue in Tests synchron aus, statt auf einen Worker zu warten.

Browser-Tests, die in einen anderen Topf gehören

Sie sind langsam, weil sie einen echten Browser steuern, und es gibt keinen Trick, der das schnell macht. Die Antwort ist Menge statt Geschwindigkeit: eine kleine Zahl über die Pfade, die zählen, getrennt von der schnellen Suite ausgeführt, damit sie nicht zwischen einer Entwicklerin und ihrer Rückmeldung stehen.

Parallelität, sobald die Tests sie verdienen

php artisan test --parallel

Nahezu lineare Verbesserung auf einer Mehrkernmaschine – und sie legt jeden Test offen, der still von einem anderen abhing. Gemeinsame Fixture-Dateien, fest verdrahtete IDs, ein Datensatz, dessen Existenz angenommen wird, weil ein früherer Test ihn angelegt hat.

Diese Fehlschläge sind es wert, gefunden zu werden. Ein Test, der nur nach einem anderen besteht, ist kein Test, sondern eine Reihenfolge, und er wäre in der CI ohnehin zufällig gescheitert.

Das Ziel

Unter einer Minute für die Suite, die man vor dem Push ausführt, und alles Langsamere – Browser-Tests, Integration gegen echte Dienste – in einem eigenen Job danach.

Der Maßstab ist nicht Abdeckung und nicht Eleganz. Er ist, ob eine Entwicklerin die Tests ausführt, ohne sich dafür zu entscheiden.

Eine Suite, die niemand ausführt, ist auch eine Suite, hinter der niemand ein Upgrade machen kann. Deshalb ist es die erste Phase eines Upgrades, eine zu bauen, und nicht die letzte. Und wenn die schnelle Suite auf SQLite läuft, während in Produktion MySQL läuft, hat dieser Tausch einen Preis, und der ist nicht immer klein.

Verwandte Fragen

Wie schnell sollte eine Suite sein?
Schnell genug, dass sie auszuführen keine Entscheidung ist. Unter einer Minute führt man sie vor jedem Push aus, ohne nachzudenken; jenseits von fünf beginnt man zu pushen und auf die CI zu warten, was die Rückmeldung von Sekunden auf Minuten verschiebt und weit mehr kostet als die gesparte Zeit.
Ist eine In-Memory-Datenbank die Lösung?
Sie ist schnell und sie ist eine andere Datenbank als die in der Produktion, was bedeutet, dass sie die Constraint-Verletzung, das Collation-Problem oder die sperrende Migration nicht findet. Vertretbar für eine Unit-lastige Suite; riskant als einziges Netz zwischen Ihnen und einem Schemafehler.
Hilft paralleles Testen?
Erheblich, und erst dann, wenn die Tests isoliert genug sind, um es zu überstehen. Alles, was in einen gemeinsamen Pfad schreibt, eine feste Datensatz-ID verwendet oder einen externen Dienst aufruft, scheitert unter Parallelität sporadisch - was ohnehin behoben gehört, denn das sind genau die Tests, die auch in der CI zufällig scheitern.
Wo sollten wir zuerst suchen?
Bei den zehn langsamsten Tests, nicht beim Durchschnitt. Suiten werden meist von einer Handvoll Fälle dominiert, die etwas Teures tun, und die zu beheben ist ein Nachmittag. Die ganze Suite auf Geschwindigkeit umzuschreiben ist es nicht.

← Zurück zu allen Artikeln

Anrufen+1 848 272 7583WhatsApp+90 850 308 5436E-Mailinfo@codefacture.comKontaktseite