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
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
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
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.
<!-- 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>
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.
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ę.
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.