Kompetenzen Portfolio Referenzen Lebenslauf Blog Termin buchen

Wie du PHPUnit lokal in PhpStorm mit Docker ausführst

Gepostet von: Felix Dziekan in: Blog am 

In diesem Artikel zeige ich dir, wie ich meine lokale PHP-Entwicklungsumgebung konfiguriert habe, damit ich Tests lokal ausführen und debuggen kann. Unit-Tests sind hier ehrlich gesagt nicht das große Problem — Funktionstests sind die, die etwas mehr Zuwendung brauchen, bevor sie laufen.

Was ist das Problem?

Die Sache ist die: Wir wollen nichts mehr für die Entwicklung auf der lokalen Maschine installieren, außer der IDE. Als PHP-Entwickler wollen wir Docker nutzen, denn Docker heißt, dass jeder im Team dieselbe Umgebung hat und "bei mir läuft's" der Vergangenheit angehört.

Damit haben wir aber eine weitere Schicht in den Stack gezogen, und jetzt liegen IDE und Code auf gegenüberliegenden Seiten einer Wand. PhpStorm will vendor/bin/phpunit ausführen. Auf deinem Laptop gibt es kein vendor/bin/phpunit — es gibt überhaupt kein PHP auf deinem Laptop. Alles muss in den Container durchgereicht werden, und die Ergebnisse müssen mit Dateipfaden zurückkommen, mit denen die IDE noch etwas anfangen kann.

PhpStorm führt den Test im php-Container aus und mappt die Ergebnisse zurück auf deine Dateien

Setup

Für dieses Setup nutze ich:

  • Symfony
  • PHPUnit
  • PHP 8.2
  • Docker und docker-compose
  • PhpStorm

Die Umgebung selbst ist die, die ich in meinem Artikel zur lokalen PHP-Entwicklungsumgebung beschrieben habe: ein php-Container aus dem Produktions-Image, ein web-Container und ein db-Container, mit dem Projekt gemountet unter /var/www/site.

Dieser Mount-Pfad ist wichtiger als alles andere in diesem Artikel. Merk ihn dir.

Schritt 1: PhpStorm den Container beibringen

Geh auf Settings → PHP und leg einen neuen CLI-Interpreter an. Wähl From Docker, Vagrant, VM, WSL, Remote…, dann Docker Compose, nimm deine docker-compose.yml und wähl den php-Service.

PhpStorm startet den Service einmal und liest die PHP-Version daraus zurück. Wenn es die richtige Version meldet, funktioniert die Verbindung. Wenn es hängt, braucht deine Compose-Datei einen Service, der am Leben bleibt — ein Container, der einen Befehl ausführt und sofort endet, taugt nicht als Interpreter.

Setz den Lifecycle auf Connect to existing container, wenn Tests in dem Container laufen sollen, der schon läuft, statt für jeden Lauf einen Wegwerf-Container zu starten. Mit einer Datenbank im Spiel willst du das fast immer.

Schritt 2: das Path Mapping

Hier gehen die meisten Setups still kaputt. PhpStorm kennt dein Projekt als /home/du/projekte/meinprojekt/project. Der Container kennt es als /var/www/site. Jeder Stacktrace, jeder Fehler, jeder Coverage-Report kommt mit Container-Pfaden zurück, und die IDE muss übersetzen.

Im Interpreter-Dialog, unter Path mappings, mappst du deinen lokalen Projektwurzelordner auf /var/www/site. Wenn deine Compose-Datei das Volume schon deklariert, erkennt PhpStorm das meist — aber prüf es, denn wenn es falsch ist, ist das Symptom zum Verrücktwerden: Tests laufen, Ergebnisse erscheinen, und ein Klick auf einen Fehler öffnet nichts.

Dasselbe Mapping ist es, das Debugging funktionieren lässt, zusammen mit dieser Zeile in docker-compose:

 

    environment:
      - PHP_IDE_CONFIG=serverName=myproject

 

Dieser serverName muss zum Namen des Servers passen, den du unter Settings → PHP → Servers konfigurierst. So sagt Xdebug PhpStorm, welches Mapping anzuwenden ist. Vertipp dich, und der Debugger verbindet sich und hält an gar nichts an.

Schritt 3: PHPUnit auf die richtige Konfiguration zeigen

Unter Settings → PHP → Test Frameworks legst du ein PHPUnit by Remote Interpreter an, wählst den eben erstellten Interpreter und nimmst Use Composer autoloader mit dem Pfad /var/www/site/vendor/autoload.php. Container-Pfad, nicht lokaler Pfad.

Dann setz die Standard-Konfigurationsdatei auf /var/www/site/phpunit.xml.dist. Ohne das führt PhpStorm PHPUnit ohne Konfiguration aus, was heißt kein Bootstrap, was heißt, dass Symfonys Kernel nie hochfährt und jeder Funktionstest stirbt, bevor er anfängt.

An diesem Punkt funktionieren Unit-Tests. Rechtsklick auf eine Testklasse, ausführen, grüner Balken. Das war die leichte Hälfte.

Die Probleme mit Funktionstests

Funktionstests booten den Symfony-Kernel, sprechen mit einer Datenbank und schreiben in den Cache. Jedes davon überquert eine Containergrenze, und alle drei haben eine Falle.

Die Rechte am Cache-Verzeichnis

Beim ersten Funktionstest bekommst du sehr wahrscheinlich einen Rechte-Fehler auf var/cache/test. Der Webserver-User im Container, der PHP-FPM-User und dein Host-User sind drei verschiedene UIDs, die in dasselbe gemountete Verzeichnis schreiben, und wer zuerst da ist, besitzt die Dateien.

Die Lösung kommt ins Dev-Dockerfile, damit sie einmal für alle gelöst ist statt eine Zeile in einer README zu sein, die niemand liest:

 

RUN mkdir -p /var/www/site/var/cache/test
RUN chmod -Rvf 777 /var/www/site/var/cache/test

 

Ja, 777. Das ist ein Container, den es nur auf Entwickler-Laptops gibt und der nirgendwohin ausgeliefert wird. Mir ist ein sperrangelweit offenes Cache-Verzeichnis in einem Dev-Image lieber als vier Entwickler, die sich je ihr eigenes chown-Ritual ausdenken.

Der Datenbank-Host ist nicht localhost

In .env.test muss die Datenbank-URL den Compose-Servicenamen nutzen, nicht localhost:

 

DATABASE_URL="mysql://root:secret@db:3306/myproject_test?serverVersion=10.5"

 

Innerhalb des Containers ist localhost der Container selbst, und dort gibt es keine Datenbank. db ist der Servicename aus docker-compose, und Dockers internes DNS löst ihn auf. Das erwischt jeden genau einmal.

Nimm außerdem eine separate Testdatenbank, nicht die für die Entwicklung. Funktionstests sollen jede Tabelle leeren dürfen, die sie anfassen, und das können sie nicht, wenn deine Entwicklungsdaten darin liegen.

Die Testumgebung muss erzwungen werden

Symfony entscheidet über APP_ENV, welche Umgebung es bootet, und wenn PhpStorm PHPUnit startet, gibt es deine nicht zwingend mit. Erzwing sie in der phpunit.xml.dist:

 


    
    

 

Das force="true" ist der wichtige Teil. Ohne es gewinnt ein APP_ENV, das von irgendwoher hereingeleckt ist, und du bekommst eine Funktionstest-Suite, die still gegen deine Entwicklungsdatenbank läuft. Das ist ein schlechter Nachmittag.

Einen Test debuggen

Sobald Interpreter und Path Mapping stimmen, braucht das Debuggen eines Tests nichts weiter. Breakpoint setzen, Rechtsklick auf den Test, Debug wählen. Xdebug ist schon im Dev-Image und schon so konfiguriert, dass es den Host auf Port 9003 zurück erreicht.

Das ist die Belohnung für die ganze Einrichtungsarbeit. In einem fehlschlagenden Funktionstest anhalten zu können, sich das tatsächliche Response-Objekt anzusehen und in den Service hineinzuspringen, der es erzeugt hat, schlägt das Lesen von var_dump-Ausgaben aus einem Terminal um einen schwer zu übertreibenden Abstand.

Eine Testsuite, die du debuggen kannst, wird benutzt. Eine Testsuite, die du nur scheitern sehen kannst, wird gelöscht.

Das eine, was du zuerst machen solltest

Wenn du eine Sache aus diesem Artikel mitnimmst: Prüf das Path Mapping, bevor du irgendetwas anderes prüfst. Fast jedes "PHPUnit funktioniert nicht in PhpStorm mit Docker"-Problem, das ich hatte, war am Ende die IDE und der Container, die sich uneinig darüber waren, wo der Code liegt.

Wenn du die Umgebung willst, auf der das aufbaut, steht die in dem Artikel zur lokalen PHP-Entwicklungsumgebung. Und falls du dich fragst, warum ich nicht einfach ein fertiges Setup nehme: darüber habe ich auch geschrieben.

Sie brauchen einen Senior-Entwickler, der liefert?

Über 20 Jahre. Produktionssysteme. Echte Deadlines. Ob Architekt, Entwickler oder DevOps-Engineer — ich habe alle drei Rollen übernommen, oft im selben Projekt. Sparen wir uns den Agentur-Overhead und sprechen direkt.