PHPUnit 6, eine neue Major Version des De-facto-Standards für das Testen von PHP-basierter Software, wurde kürzlich veröffentlicht. Kurz darauf funktionierten die Tests von einigen Entwicklerinnen und Entwicklern, die noch nicht auf die neue Version umsteigen wollten, nicht mehr. Was war passiert?
Diese Entwicklerinnen und Entwicklern wurden von den Konsequenzen der Verwendung des "Todessterns", des * Operators, in ihrer composer.json Datei überrascht.
Weder * noch unbeschränkte Bereiche wie >= 1.0 können wirklich als Versionseinschränkungen betrachtet werden.
Wenn du
{
"require-dev": {
"phpunit/phpunit": "*"
}
}
oder
{
"require-dev": {
"phpunit/phpunit": ">= 4.8"
}
}
verwendest, dann fragst du nach Problemen, da du Composer anweist, die neueste Version von PHPUnit zu installieren. Dies könnte eine neue Major Version sein, wie es bei PHPUnit 6.0.0 der Fall war, die nicht abwärtskompatibel ist und die deine Tests ohne Migrationsaufwand nicht ausführen kann.
Semantic Versioning
Die meisten Komponenten, die du per Composer installieren kannst, verwenden Semantic Versioning. Kurz gesagt bedeutet dies, dass du dich bei der Interpretation ihrer Versionsnummern auf die folgenden drei einfachen Regeln verlassen kannst:
- Die Major Version wird inkrementiert, wenn es inkompatible Änderungen gibt, beispielsweise wenn sich die öffentliche API ändert. PHPUnit zum Beispiel erhöht die Major Version auch, wenn es die Unterstützung für eine PHP-Version einstellt, die zuvor unterstützt wurde.
- Die Minor Version wird inkrementiert, wenn neue Funktionalität abwärtskompatibel hinzugefügt wird.
- Die Patch Version wird inkrementiert, wenn Fehler abwärtskompatibel behoben werden
Composer unterstützt die Idee der semantischen Versionierung durch den Caret Operator (^):
{
"require-dev": {
"phpunit/phpunit": "^5.7"
}
}
Mit der oben gezeigten Versionseinschränkung ^5.7 weist du Composer an, die neueste Version von PHPUnit zu installieren, die mit PHPUnit 5.7 abwärtskompatibel ist.
Abhängigkeiten frisch halten
Vor ein paar Jahren habe ich geschrieben:
Es ist sehr sinnvoll, bei der Entwicklung Ihrer Software Komponenten von Drittanbietern zu verwenden. Wenn du jedoch eine Komponente verwendest, die nicht (ausreichend) getestet ist, gehst du das gleiche Risiko ein wie der Koch, der eine Fertigsauce aus dem Supermarkt verwendet. Du weißt nicht genau, was der Code der Komponente tut. Für den "Happy Path" mag sie gut funktionieren. Aber was ist mit den Randfällen? Noch schlimmer ist es, eine Komponente eines Drittanbieters zu verwenden, die nicht mehr gewartet wird. Sie mag heute gut funktionieren. Aber wird sie auch noch funktionieren, wenn beispielsweise eine neue Version von PHP veröffentlicht wird?
Was ich damals geschrieben habe, ist immer noch gültig. Aber es gibt einen Punkt, den ich damals nicht gemacht habe: Halte deine Abhängigkeiten frisch. Als die aktive Unterstützung für PHP 5 endete, schrieb ich:
Ein Upgrade der von dir verwendeten PHP-Version darf kein seltenes Ereignis sein, vor dem du dich fürchtest. Du darfst das Upgrade deines PHP-Stacks nicht als "besonderes Projekt" betrachten. Du musst das Upgrade der von dir verwendeten PHP-Version zu einem Teil deiner normalen Arbeitsabläufe machen und den Upgrade-Zyklus deines PHP-Stacks mit dem Release-Zyklus des PHP-Projekts abstimmen.
Das Gleiche gilt für alle Komponenten von Drittanbietern, die du verwendest: Halte sie frisch, um deine Software gesund zu halten.
Wenn du Composer verwendest, um die Abhängigkeiten deines Projekts zu verwalten, solltest du folgende Versionsbeschränkung verwenden:
{
"require-dev": {
"phpunit/phpunit": "^6.0"
}
}
Mit dieser Konfiguration installiert Composer immer die neueste Version von PHPUnit, die mit PHPUnit 6.0 kompatibel ist.
Dies stellt sicher, dass du "frisch" bleibst, solange PHPUnit 6 die aktuelle stabile Version von PHPUnit ist. Und wenn die Zeit kommt und PHPUnit 7 veröffentlicht wird, wird Composer es nicht automatisch und unerwartet installieren.
Das Upgraden einer Abhängigkeit auf eine neue Major Version muss eine bewusste Entscheidung sein, die Teil eines definierten Prozesses ist. Dieser Prozess sollte zumindest das Lesen des ChangeLogs beinhalten.