Testowanie Laravel z Pest, część 5: Testy architektury, CI i pokrycie

7 min. czytaniaZaktualizowano

Testowanie Laravel z Pest — Część 1 · Część 2 · Część 3 · Część 4 · Część 5

Testy architektury mogą chronić kontrolery przed zależeniem od infrastruktury albo wymuszać reguły nazewnictwa, lecz uzupełniają testy zachowania, nie zastępują ich. Lokalnie uruchamiaj skupiony suite, a w CI pełny suite, analizę statyczną i formatowanie. Testowanie równoległe skraca feedback, gdy izolacja jest poprawna. Pokrycie traktuj jako mapę niewykonanego kodu, nie cel dowodzący poprawności.

Umieść trwałe reguły projektu w osobnym teście

Testy behawioralne mówią, czy klient może utworzyć zamówienie. Nie mówią, czy kontroler pozostał granicą transportu podczas implementacji tej funkcji. To jest użyteczne zadanie testu architektury: wyrazić małą, stabilną decyzję, którą w przeciwnym razie trzeba byłoby odkrywać ponownie w każdym review.

W tej aplikacji kontrolery HTTP mogą używać requestów, resources i actions, ale nie powinny samodzielnie pytać Eloquent ani wołać klienta dostawcy. Nie jest to abstrakcyjna preferencja dla warstw. Oznacza, że późniejsze API, komenda CLI albo job z kolejki wykorzysta decyzję o zamówieniu bez kopiowania pracy HTTP.

Trzymaj te testy w wydzielonym pliku. Złamana reguła powinna wskazać decyzję projektową, która zawiodła, zamiast zniknąć pośród testów endpointów.

php
<?php

declare(strict_types=1);

arch('HTTP controllers are thin')
    ->expect('App\\Http\\Controllers')
    ->toOnlyUse([
        'App\\Http\\Requests',
        'App\\Http\\Resources',
        'App\\Actions',
        'Illuminate',
    ]);

arch('HTTP controllers do not reach persistence')
    ->expect('App\\Http\\Controllers')
    ->not->toUse('Illuminate\\Database')
    ->not->toUse('App\\Models');

arch('actions have an explicit name')
    ->expect('App\\Actions')
    ->toHaveSuffix('Action')
    ->toBeClasses()
    ->not->toBeAbstract();

Dokładna reguła jest lokalną decyzją projektową, a nie przykazaniem Laravela. Kontroler invokable może rozsądnie używać typu Order do route model bindingu. Ogólny zakaz modeli odrzuciłby prawidłowy kod frameworka i nauczył zespół omijania suite'u. Przenieś wiązanie do requestu lub action tam, gdzie to pomaga, albo dopuść wąski, opisany wyjątek. Reguła ma utrudniać niechcianą zmianę, nigdy nie czynić zwykłego kodu nieczytelnym.

Testy architektury chronią także kierunek zależności. Jeżeli App\\Domain zawiera przejścia stanów zamówienia, a App\\Infrastructure zawiera repozytoria Eloquent i adaptery księgowe, serwis domenowy może opisać port, ale nie może importować adaptera.

php
<?php

declare(strict_types=1);

arch('domain code does not depend on infrastructure')
    ->expect('App\\Domain')
    ->not->toUse('App\\Infrastructure')
    ->not->toUse('Illuminate\\Http')
    ->not->toUse('Illuminate\\Support\\Facades');

arch('infrastructure classes reveal their role')
    ->expect('App\\Infrastructure')
    ->toHaveSuffix(['Repository', 'Client', 'Gateway'])
    ->toBeClasses();

Nie twórz tych namespace'ów wyłącznie po to, aby napisać sprytną asercję. Mała aplikacja z jedną klasą CreateOrderAction i konwencjonalnymi modelami Eloquent może mieć kontroler, action i model jako prawdziwą granicę. Dodaj testy architektury, gdy ten sam błąd warstwowania wydarzy się drugi raz, granica pakietu ma znaczenie albo zespół potrzebuje wspólnej reguły. To nie są testy bezpieczeństwa ani analizy typów: policies i feature testy dowodzą dostępu, analiza statyczna znajduje niemożliwe typy, a test architektury daje szybki feedback strukturalny.

Pogrupuj feedback, potem zabezpiecz stan dla procesów równoległych

Jeden ogromny suite jest wolny i niejednoznaczny. Nazwij rodzaje dowodów, ale nie duplikuj każdego przykładu w wielu grupach. Skupiona komenda developera ma być szybka; chroniony branch nadal uruchamia cały kontrakt produktu. Testy przeglądarkowe zasługują na grupę, ponieważ ich runtime jest cięższy, a ich wartością jest kilka drogich ścieżek użytkownika.

php
<?php

declare(strict_types=1);

use App\\Models\\User;
use Illuminate\\Foundation\\Testing\\RefreshDatabase;

uses(RefreshDatabase::class)->group('feature')->in('Feature');

test('a customer can submit a support request', function (): void {
    $this->actingAs(User::factory()->create())
        ->post('/support/requests', [
            'subject' => 'Invoice copy',
            'category' => 'billing',
            'message' => 'Please send the invoice for order 1001.',
        ])
        ->assertRedirect();
})->group('browser-contract');

Opcja Laravela --parallel poprawia feedback dopiero wtedy, gdy procesy posiadają własny stan: bazę, prefiks cache'a, ścieżkę filesystemu i każdy zasób, do którego zapisują. Suite przechodzący seryjnie, ale zawodzący równolegle, odkrył prawdziwy defekt izolacji. Nie odkrył powodu do retry ani do wyłączenia parallel mode.

xml
<!-- phpunit.xml -->
<php>
    <env name="APP_ENV" value="testing"/>
    <env name="CACHE_STORE" value="array"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="FILESYSTEM_DISK" value="testing"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
</php>
bash
php artisan test --compact
php artisan test --compact --parallel --processes=4

Twardo wpisany e-mail może zderzyć się z unikalnym indeksem. Test zapisujący reports/latest.pdf ściga drugi proces. Wspólny klucz Cache::remember('current-order', ...) może zwrócić wartość innego przykładu. Generuj unikalne fixture'y, prefiksuj klucze ID zamówienia, używaj Storage::fake() i twórz mutowalny stan wewnątrz każdego przykładu.

Równoległość nie jest shardingiem CI. Równoległość dzieli jedną komendę na jednym runnerze; sharding dzieli suite między osobne joby. Używaj pierwszego, gdy runner ma wolne CPU. Shardy dodaj dopiero po pomiarze stale wolnego suite'u: cztery joby głównie bootujące Composera i usługi kosztują więcej bez szybszego wartościowego dowodu.

Niech CI zbiera dowód we właściwej kolejności

CI powinno najpierw zawodzić na tanich, deterministycznych kontrolach, a potem uruchamiać zachowanie. Ten workflow GitHub Actions utrzymuje widoczne formatowanie i analizę statyczną, testuje wspierane PHP, dzieli suite aplikacji i zostawia kontrakt przeglądarkowy w osobnym jobie.

yaml
name: tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['8.4', '8.5']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: none
      - uses: ramsey/composer-install@v3
      - run: vendor/bin/pint --test
      - run: vendor/bin/phpstan analyse --memory-limit=1G

  tests:
    needs: quality
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.5'
          coverage: none
      - uses: ramsey/composer-install@v3
      - run: php artisan test --compact --parallel --shard=${{ matrix.shard }}/4

  browser-contract:
    needs: quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.5'
      - uses: ramsey/composer-install@v3
      - run: npm ci
      - run: php artisan test --compact --group=browser-contract

Zanim skopiujesz flagi shardingu do produkcyjnego workflow, sprawdź zainstalowane wersje Pest i Laravela; składnia i wsparcie runtime'u się zmieniają. Projekt nie: każdy shard uruchamia rozłączny, deterministyczny fragment suite'u, a branch protection wymaga każdego z nich. Jeden zielony shard z czterech nie jest zielonym suite'em. Testuj każdą wersję PHP obiecaną przez pakiet, ale dla aplikacji wdrażanej na jednym runtime nie twórz szerokiej macierzy z przyzwyczajenia.

Traktuj pokrycie jak pytanie

Pokrycie pyta, dlaczego gałąź nie została wykonana. Nie dowodzi, że wykonana linia została sensownie sprawdzona. Generuj raport HTML podczas badania zmiany, zamiast każdemu pull requestowi kazać czekać na instrumentację.

bash
XDEBUG_MODE=coverage php artisan test --compact --coverage-html=build/coverage

Niepokryta gałąź forbidden powinna uruchomić test HTTP dla managera z innego konta. Niepokryty brak stanu magazynowego wymaga testu domenowego albo feature. Test, którego jedynym celem jest wywołanie Order::getKey(), kupuje procent, nie bezpieczeństwo. Progi pokrycia mogą pasować do dojrzałego pakietu z małym API. Są słabym pierwszym celem dla zmieniającej się aplikacji, migracji, generowanego kodu lub zespołu, który uczy się, czego dowodzą testy.

Kontrakt dostarczania ma zatem warstwy: testy zachowania, kilka ścieżek przeglądarkowych, odizolowane granice zewnętrzne, trwałe reguły architektury, czyste CI i review kierowane pokryciem. Pipeline zasługuje na zaufanie, gdy czerwony wynik jest konkretny i odtwarzalny, a zielony znaczy, że ważny kontrakt rzeczywiście się uruchomił.

Zanim uczynisz którąkolwiek z tych kontroli wymaganą, pozwól zespołowi zobaczyć kilka błędów i uzgodnić drogę naprawy. Reguła kontrolera potrzebuje oczywistej akcji przenoszącej pracę; niestabilny test równoległy potrzebuje odtwarzalnej poprawki fixture'u; raport pokrycia potrzebuje osoby, która odróżni martwy kod od brakującego pokrycia ryzyka. W przeciwnym razie CI stanie się bramką, której ludzie nie lubią, zamiast szybkim, wspólnym review kontraktu dostarczania. Trzymaj regułę, komendę i komunikat błędu na tyle blisko siebie, by następny współtwórca poprawił problem bez zgadywania.

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.