Testy i CI dla pakietów Laravel

8 min. czytania

Zestaw testów pakietu musi udowodnić integrację, nie tylko metody jednostkowe. Orchestra Testbench bootuje małą aplikację Laravel bez wymagania repozytorium użytkownika.

php
<?php

declare(strict_types=1);

namespace DominikDev\WebhookTap\Tests;

use DominikDev\WebhookTap\WebhookTapServiceProvider;
use Orchestra\Testbench\TestCase;

abstract class PackageTestCase extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [WebhookTapServiceProvider::class];
    }
}

Pisz testy Pest dla obserwowalnego kontraktu: dostarczenie jest zapisane, opublikowana konfiguracja zmienia zachowanie, fasada rozwiązuje skonfigurowany serwis, a webhook:replay kończy się powodzeniem. Migracje bazy definiuj w bazowym przypadku testowym, zamiast zakładać, że developer zmigrował lokalną aplikację.

CI powinno testować obietnicę wsparcia. Zbuduj macierz PHP 8.3–8.5 z obsługiwanymi wydaniami Laravela, uruchom zwykły lockfile oraz job --prefer-lowest, aby wykryć zbyt optymistyczne ograniczenia. Dodaj Pint, PHPStan/Larastan i Rector tylko wtedy, gdy ich konfiguracja jest utrzymywana; czerwony badge z porzuconej bazy analizy statycznej jest gorszy niż brak badge'a.

Nie umieszczaj w testach prawdziwych poświadczeń sieciowych. Zamockuj gateway za kontraktem i uwzględnij przypadki błędów oraz retry: szczególnie pakiety webhookowe muszą umożliwiać testowanie błędów podpisu i zduplikowanych dostarczeń.

Bootuj granicę, którą otrzymuje użytkownik

Pakiet potrzebuje dwóch rodzajów pewności. Test jednostkowy szybko chroni parser albo algorytm podpisu. Test integracyjny dowodzi, że aplikacja użytkownika odkryje providera, scali konfigurację, załaduje migracje, rozwiąże bindingi i uruchomi komendę. Zielone unity nie wykryją literówki w providerze, brakującego tagu publish ani migracji pominiętej w archiwum.

Zdefiniuj jeden Testbench base case z jawnymi zależnościami pakietu. Nie dziedzicz po przypadku testowym prawdziwej aplikacji: ukryta konfiguracja może sprawić, że pakiet przejdzie testy z niewłaściwego powodu. Testbench uruchamia małą aplikację Laravel, ale zostawia widoczną granicę integracji.

php
<?php

declare(strict_types=1);

namespace DominikDev\WebhookTap\Tests;

use DominikDev\WebhookTap\WebhookTapServiceProvider;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Orchestra\Testbench\TestCase;

abstract class PackageTestCase extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [WebhookTapServiceProvider::class];
    }

    protected function defineDatabaseMigrations(): void
    {
        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');

        Schema::create('webhook_deliveries', function (Blueprint $table): void {
            $table->id();
            $table->string('external_id')->unique();
            $table->text('payload');
            $table->timestamps();
        });
    }
}

Hook migracji jest kontrolą wydania, nie ozdobą. Bez niego lokalna baza może przypadkowo zazielenić testy, gdy czysty proces CI polegnie. Ładuj prawdziwe migracje, jeśli są publicznym assetem pakietu; w base case twórz wyłącznie małe tabele harnessu. SQLite jest dobre dla szybkich kontraktów, ale nie dowodzi semantyki JSON, indeksów ani locków MySQL. Jeśli to część obietnicy, dodaj ukierunkowany job bazodanowy.

Testuj kontrakt obserwowalny przez konsumenta

Test powinien dowodzić, że serwis się rozwiązuje, poprawne dostarczenie jest zapisywane, konfiguracja zmienia zachowanie, zły podpis kończy się błędem, a komenda zwraca użyteczny kod wyjścia. Te nazwy mogą trafić do README. Nie sprawdzaj prywatnych właściwości ani kolejności metod providera: takie testy utrudniają bezpieczny refactor i nie łapią zepsutej adopcji.

php
<?php

declare(strict_types=1);

use DominikDev\WebhookTap\Exceptions\InvalidSignature;
use DominikDev\WebhookTap\WebhookRecorder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(\DominikDev\WebhookTap\Tests\PackageTestCase::class, RefreshDatabase::class);

it('stores a verified delivery through the container binding', function (): void {
    config()->set('webhook-tap.secret', 'test-secret');

    $payload = '{"id":"evt_123","type":"invoice.paid"}';

    app(WebhookRecorder::class)->record(
        payload: $payload,
        signature: hash_hmac('sha256', $payload, 'test-secret'),
    );

    $this->assertDatabaseHas('webhook_deliveries', [
        'external_id' => 'evt_123',
        'payload' => $payload,
    ]);
});

it('rejects a delivery with an invalid signature', function (): void {
    config()->set('webhook-tap.secret', 'test-secret');

    app(WebhookRecorder::class)->record('{"id":"evt_123"}', 'not-a-signature');
})->throws(InvalidSignature::class);

it('does not store the same external delivery twice', function (): void {
    config()->set('webhook-tap.secret', 'test-secret');

    $payload = '{"id":"evt_123"}';
    $signature = hash_hmac('sha256', $payload, 'test-secret');
    $recorder = app(WebhookRecorder::class);

    $recorder->record($payload, $signature);
    $recorder->record($payload, $signature);

    expect(\DB::table('webhook_deliveries')->count())->toBe(1);
});

Duplikat nie jest przypadkiem brzegowym: dostawcy ponawiają żądania. Stałe payloady i obliczony HMAC dowodzą kontraktu bez niestabilnej sieci i sekretów. Dla adaptera HTTP użyj testów żądań Laravela i sfałszuj kontrakt downstream. Dla kolejki sprawdź dispatch oraz osobno błąd i retry handlera; dispatch joba nie dowodzi poprawności jego wykonania.

Lokalne polecenia i CI muszą robić to samo

Nie każ współtwórcy odgadywać komend CI. Umieść stabilne polecenia w skryptach Composera i wywołuj je w workflow. Wersje poniżej pokazują kształt; rzeczywiste constrainty Testbench, Pest i Larastan muszą odpowiadać deklarowanemu zakresowi PHP i Laravela.

json
{
  "require-dev": {
    "orchestra/testbench": "^9.0",
    "pestphp/pest": "^3.0",
    "larastan/larastan": "^3.0",
    "laravel/pint": "^1.0"
  },
  "scripts": {
    "test": "pest --compact",
    "test:coverage": "XDEBUG_MODE=coverage pest --coverage --min=80",
    "analyse": "phpstan analyse --memory-limit=512M",
    "lint": "pint --test"
  }
}

Pokrycie jest sygnałem, nie dowodem. Procent może rosnąć, gdy błędy podpisu, migracje i gałęzie konfiguracji nadal nie są testowane. Zacznij od kontraktów konsumenta; próg coverage dodaj, gdy chroni przed regresją. Pint i Pest to mała baza. PHPStan lub Larastan dodaj tylko wtedy, gdy ktoś będzie utrzymywał ich konfigurację — trwale czerwony baseline jest gorszy od braku badge'a.

CI weryfikuje deklarację wsparcia

Macierz ma odzwierciedlać manifest i README. Jeśli pakiet deklaruje PHP 8.3–8.5 oraz Laravel 11–13, każda linia wydania potrzebuje świadomie wybranej kontroli. Formatowanie i analiza mogą działać raz na najnowszym runtime; powielanie ich w każdej komórce marnuje czas bez sprawdzenia nowej granicy zgodności. Wyłącz fail-fast, aby jedna awaria nie ukryła kolejnej.

yaml
name: tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.3', '8.4', '8.5']
        laravel: ['11.*', '12.*', '13.*']
        exclude:
          - php: '8.3'
            laravel: '13.*'
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: \${{ matrix.php }}
          coverage: none
      - run: composer require "illuminate/support:\${{ matrix.laravel }}" --no-update
      - run: composer update --prefer-dist --no-interaction
      - run: composer test

Nie kopiuj macierzy bez realnego resolve. Composer rozwiązuje cały graf, a każda linia Laravela ma własne ograniczenia PHP. Osobne pliki constraintów albo narzędzie do macierzy są dobrą alternatywą, gdy polecenia inline są nieczytelne. Istotne jest dowodliwe zainstalowanie i boot każdej reklamowanej linii.

Dodaj job prefer-lowest dla najstarszej kombinacji. Wykrywa on kod zależny od transitive dependency nowszej niż deklarowane minimum. Jest wolniejszy i może ujawnić błędy resolvera upstream, więc praktycznym wyborem bywa wymagany job harmonogramowy, nie każdy push. Nie podnoś constraintu automatycznie; najpierw zdecyduj, czy obietnica wsparcia faktycznie się zmieniła.

Kiedy nie dokładać narzędzi

Class-not-found wyłącznie w CI może oznaczać klasę wykluczoną z archiwum. Awaria świeżej migracji może znaczyć, że Testbench jej nie załadował. Błąd braku pamięci w analizie najpierw powtórz z ograniczonym wyższym limitem, a dopiero potem diagnozuj; zmniejszenie skanu jedynie ukrywa problem. Flaky HTTP test oznacza, że prawdziwa sieć wyciekła do zestawu, nie że workflow powinien retryować aż do zieleni.

Nie dodawaj mutation testingu, browser testów, Dockerowych baz i wszystkich narzędzi jakości odruchowo. Są przydatne, gdy konkretny risk uzasadnia ich czas i utrzymanie. Prywatny pakiet wydawany z jedną aplikacją może bardziej potrzebować jej integration suite niż publicznej macierzy. Reużywalny pakiet potrzebuje deterministycznych kontraktów Testbench i uczciwej macierzy wsparcia. To wiarygodna alternatywa dla zdania „działało na laptopie maintainera”.

Awarię CI traktuj jak informację o produkcie

Zachowaj polecenie, wersje zależności i driver bazy z nieudanego joba. Test przechodzący po ślepym retry nadal oznacza niekontrolowaną zależność. Odtwórz go w czystym Composer install, a potem rozstrzygnij, czy błąd jest w pakiecie, w deklaracji zgodności czy w infrastrukturze. Wyłączenie kontroli tylko zazielenia badge, lecz pozostawia użytkownika bez ochrony.

Pełną macierz uruchamiaj przed tagiem i ponownie, gdy aktualizacja bezpieczeństwa zmienia rozwiązywany graf. Nie przypinaj trwale każdej zależności przechodniej: opóźnia to poprawki bezpieczeństwa. Zachowaj lockfile dla powtarzalnego rozwoju, testuj świeże resolution w CI i publikuj constrainty zgodne z faktycznie zweryfikowanymi kombinacjami. Dla wewnętrznego pakietu z jedną wersją aplikacji wystarczy węższa polityka, ale wtedy usuń szersze constrainty i nazwij ją uczciwie.

Bramka wydania, którą da się powtórzyć

Przed tagiem uruchom normalny zestaw na czystym checkout, job z najstarszymi wspieranymi zależnościami, obejrzyj archiwum Composera i zainstaluj tag w jednorazowej aplikacji Laravel. Zapisz w release notes albo artefakcie CI wersje PHP, Laravela i drivera bazy użyte w tej weryfikacji. To najmniejszy użyteczny dowód, że działa opublikowany pakiet, a nie wyłącznie repozytorium maintainera.

Nie zamieniaj każdego wydania w tydzień ceremonii. Zmiana samej dokumentacji nie potrzebuje pełnej macierzy baz danych, lecz zmiana providera, migracji albo constraintu już tak. Chodzi o proporcjonalną weryfikację: testy najbliższe ścieżce konsumenta uruchamiają się najczęściej, wolniejsze kontrole zgodności przed wydaniem lub według harmonogramu, a awarie pozostają widoczne, dopóki ich zakres nie jest zrozumiały. Taki rytm utrzymuje zaufanie do pakietu, nie czyniąc utrzymania nieopłacalnym.

W praktyce warto też rozdzielić regresję produktu od awarii harnessu. Niedziałający serwis bazy w runnerze, błąd resolvera albo brak rozszerzenia PHP nie mówią automatycznie, że kontrakt pakietu jest błędny. Zapisz komunikat i minimalne polecenie odtwarzające problem, następnie napraw infrastrukturę lub oznacz job jako niedostępny. Nie ogłaszaj jednak pakietu zgodnym tylko dlatego, że job pominięto. Użytkownikowi potrzebny jest stan weryfikacji, nie optymistyczna interpretacja czerwonego badge'a.

Najprostszą alternatywą dla rozbudowanej macierzy jest jawnie wąski kontrakt: jedna wersja PHP, jedna aktualna linia Laravela i integracja sprawdzana przez aplikację nadrzędną. To rozsądne dla prywatnego artefaktu. Staje się problemem, gdy manifest nadal deklaruje trzy majory frameworka. Zakres w README, Composerze i workflow musi mówić to samo; w przeciwnym razie CI nie jest ochroną, lecz kolejnym miejscem, w którym można zgadywać.

Ostatnia zasada jest prosta: nie ukrywaj niezweryfikowanego zakresu za ogólnym stwierdzeniem o kompatybilności. Jeśli najniższy job nie działa, nazwij go niezweryfikowanym i napraw go przed kolejnym wydaniem. Jeśli pakiet wymaga konkretnego rozszerzenia PHP albo silnika bazy, sprawdzaj tę zależność wcześnie w CI oraz podaj ją przed poleceniem instalacji. Dzięki temu użytkownik otrzymuje czytelny błąd na początku, a nie kosztowną awarię po wdrożeniu.

To właśnie jest mierzalna, uczciwa jakość pakietu: wykrywalna przed instalacją, powtarzalna w CI i zrozumiała dla osoby odpowiedzialnej za wdrożenie.

Powiązane artykuły

Wsparcie istniejącego systemu

Potrzebujesz pomocy z działającą aplikacją?

Pomagam firmom rozwijać działające systemy, porządkować wdrożenia i dodawać nowe funkcje bez dokładania chaosu do projektu.

Komentarze (0)
Zaloguj się, aby dodać komentarz

Musisz być zalogowany, aby dodać komentarz.

Zaloguj się

Potrzebujesz kogoś, kto weźmie odpowiedzialność za kolejny krok?

Porozmawiajmy o Twoim projekcie i określmy zakres, który ma sens dla Twoich celów.