Octibiz
Demo

Durchsucht Website und Dokumentation gemeinsam. Enter zeigt alle Treffer, Esc schließt.

Plugin testen

Der wichtigste Fallstrick

Eine frische Testdatenbank hat alle Plugin-Module ausgeschaltet.

Der Schalter eines Plugins ist fail-closed: Fehlt die Zeile, gilt das Modul als aus. Eine frisch aufgesetzte Testdatenbank hat keine dieser Zeilen. Alle Routen deines Plugins antworten also mit 404, und dein Funktionstest prüft nichts.

Das Tückische daran ist, dass er trotzdem grün wird, wenn er auf einen Fehlerfall prüft. Ein Test, der einen 403 erwartet, bekommt einen 404 und ist zufrieden, wenn er nur „kein 200" prüft.

Die Gegenmaßnahme hängt davon ab, wovon dein Test erbt:

ApiTestCase — alles unter tests/Api/. Du tust nichts. make test-db legt die Schalter an, und ApiTestCase::setUp() prüft einmal je Prozess nach, ob sie tatsächlich dastehen. Fehlen sie, bricht der Lauf mit einer Anleitung ab, statt still 404 zu liefern.

KernelTestCase — alles unter tests/Functional/. Dort greift diese Prüfung nicht. Nimm SeedsPluginModuleFlagsTrait dazu und ruf seedPluginModuleFlags() unmittelbar nach self::bootKernel() auf:

use App\Tests\Support\Plugin\SeedsPluginModuleFlagsTrait;

final class ShipmentServiceTest extends KernelTestCase
{
    use SeedsPluginModuleFlagsTrait;

    public function testEtwas(): void
    {
        self::bootKernel();
        $this->seedPluginModuleFlags();

        // ab hier gilt dein Modul als eingeschaltet
    }
}

Das Merkmal setzt am Ende die gemerkte Sicht des ModuleGate zurück. Ohne diesen Rücksetzer hält ein gemerktes „alles aus" fest, obwohl die Zeilen inzwischen da sind.

Tests liegen nicht im Plugin

PHP-Tests gehören zentral unter tests/, gespiegelt nach Modul. Ein Test unter plugins/<Name>/tests/ wird nie ausgeführt: Die Testkonfiguration bindet ihn nicht ein.

Ein Test, den der Testläufer nicht findet, ist kein Test. Ein Architektur-Wächter hält das fest, damit es nicht wieder passiert.

Ausnahme: Ein Fremd-Plugin mit eigenem Repository bringt seine eigene Testkonfiguration mit. Das Gerüst legt sie samt Beispieltest und Arbeitsablauf für die fortlaufende Prüfung an.

Oberflächen-Tests sind die andere Ausnahme. Sie liegen sehr wohl im Plugin, unter frontend/**/__tests__/, und werden von dort eingesammelt.

Die Testdatenbank vorbereiten

make test-db

Das leert die Testdatenbank, fährt die Migrationen und bereitet das Schema vor. Nötig nach jeder Schemaänderung, auch nach einer aus einem fremden Plugin.

Wenn nach einem Verschieben von Dateien plötzlich Datenmodell-Fehler auftauchen, die keinen Sinn ergeben, sind es meist alte Zwischenstände:

vendor/bin/phpstan clear-result-cache
bin/console cache:clear --env=test

Was es an Basisklassen gibt

BasisklasseWofürVerbreitung
ApiTestCaseEchte Anmeldung mit Token, Transaktion je Test, Nutzer mit gezielten Rechten925 Tests
KernelTestCaseDienste ohne HTTP-Schicht454 Tests
ModuleSecurityTestCaseErbt von ApiTestCase, dazu collectUlids(), lastStatus(), lastJson(), reload()60 Tests
PortalIsolationTestCaseAbgrenzung im Kundenportal24 Tests
PortalApiTestCaseAnmeldung im Kundenportal-Bereich8 Tests

ApiTestCase ist der Normalfall und gibt dir zwei Methoden an die Hand:

protected function seedUser(array $permissions, ?int $brandId = null): User;
protected function clientFor(User $user): Client;
protected function addMembership(User $user, array $permissions, ?int $brandId): Membership;

Der Wert steckt im ersten Parameter: ein Nutzer mit genau den Rechten, die du prüfen willst. Denn die meisten echten Sicherheitslücken entstehen nicht bei fehlendem Recht, sondern bei zu weitem.

Der Test, der die meisten Fehler findet

Das mit Abstand häufigste Fehlermuster im Bestand war nicht die fehlende Rechteprüfung, sondern der Marken-Schnitt an der Mitgliedschaft statt am Recht: Wer in einer zweiten Marke Mitglied ist, aber dort kein Recht hat, sah trotzdem deren Daten.

Deshalb gehört zu jeder markenbezogenen Liste ein Test nach diesem Muster:

public function testFremdmarkeFliesstNichtInDieSumme(): void
{
    // Zwei Belege, zwei Marken.
    ShipmentFactory::createOne(['brandId' => self::MARKE_A, 'amount' => 10_000]);
    ShipmentFactory::createOne(['brandId' => self::MARKE_B, 'amount' => 99_900]);

    // Recht NUR in Marke A. In Marke B besteht eine Mitgliedschaft ganz OHNE Rechte.
    $nutzer = $this->seedUser(['acme.shipping.view'], self::MARKE_A);
    $this->addMembership($nutzer, [], self::MARKE_B);

    $client = $this->clientFor($nutzer);
    $client->request('GET', '/api/v1/acme-shipping/shipments');

    $daten = $client->getResponse()->toArray();
    self::assertSame(10_000, $daten['summe'], 'Der Bestand der Marke B darf nicht in der Summe stecken.');
    self::assertSame(1, $daten['anzahl'], 'Auch die ANZAHL verraet den Fremdbestand nicht.');
}

Die zweite Zusicherung ist die, die man vergisst. Eine Summe zu verbergen und die Anzahl mitzuliefern verrät den fremden Bestand trotzdem.

Befehle

make test                 # alles: Backend und Oberflaeche
make test-api             # nur die API-Strecke
make lint stan            # Stil und statische Analyse
make gate                 # das vollstaendige Tor, vor jedem "fertig"

vendor/bin/phpunit tests/Api/AcmeShipping           # gezielt
cd frontend && npx vitest run ../plugins/AcmeShipping/frontend

make gate ist der maßgebliche Lauf. Er fasst alle Prüfungen zusammen, und er ist die Antwort auf die Frage, ob etwas fertig ist.

Vor dem Ausliefern

Geprüft?Was
Schema wandert nur additiv, keine Inhalte in Migrationen
Standarddaten, Rechte und Nachschlagewerte kommen aus den Verträgen
Jede Einzel-Operation hat einen eigenen Voter
Jede markenbezogene Liste ist auf das Recht geschnitten, nicht auf die Mitgliedschaft
Jeder Sammel-Filter ist als Parameter deklariert und damit sichtbar
Ein Demo-Beitrag existiert, wenn das Plugin eine eigene Fachtabelle hat
Jede Spalte mit Personenbezug hat einen Beitrag zu Auskunft und Löschung
Kopplungen an andere Plugins sind deklariert
Der Modul-Schalter wird vom Plugin selbst mitgeliefert
Der Generator-Rest ist raus
Version steht überall gleich: Descriptor, Paketdateien, Änderungsprotokoll
make gate ist grün

Weiter

plugin.testing · Gilt ab Version 0.5.0