Testowanie Laravel z Pest — Część 1 · Część 2 · Część 3 · Część 4 · Część 5
Test feature potrafi potwierdzić, że kontroler zwraca właściwe przekierowanie i że rekord trafia do bazy. Nie dowiedzie jednak, że nieaktywny przycisk staje się aktywny, nawigacja Inertia renderuje stan sukcesu albo nieobsłużony błąd JavaScriptu przerwał ścieżkę po drodze. Testy przeglądarkowe obejmują tę ostatnią granicę: stronę, którą człowiek rzeczywiście widzi w prawdziwej przeglądarce.
Nie znaczy to, że powinny być domyślnym testem każdego endpointu. Są wolniejsze, wymagają środowiska przeglądarki i dają mniej skupioną diagnozę niż test feature. Zostaw je dla niewielkiej liczby kosztownych ścieżek: logowania, publikacji wpisu, płatności albo złożonego formularza z istotnym JavaScriptem. Macierze walidacji, polityki i reguły domenowe trzymaj w szybkich testach unit oraz HTTP.
Budujemy tu test przeglądarkowy tworzenia zgłoszenia do wsparcia. Ważniejsza od konkretnego API jest dyscyplina: deterministyczny stan, selektory zrozumiałe dla użytkownika, obserwowalne oczekiwanie oraz asercje ujawniające awarię JavaScriptu, a nie tylko obecność strony.
Zacznij od ścieżki, nie od listy ekranów
Załóżmy, że zalogowany klient wysyła zgłoszenie. Formularz ma temat, kategorię i wiadomość. Wysłanie obsługuje JavaScript, a użytkownik dostaje widoczne potwierdzenie. Obietnica nie brzmi „formularz ma trzy pola”. Brzmi: klient może wysłać prawidłowe zgłoszenie, widzi potwierdzenie i nie napotyka błędu przeglądarki.
Trasa oraz kontroler nadal zasługują na zwykłe testy feature. Test browserowy jest cienkim, końcowym dowodem, że backend i frontend mówią tym samym językiem.
<?php
declare(strict_types=1);
use App\Http\Controllers\SupportRequestController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function (): void {
Route::get('/support/requests/create', [SupportRequestController::class, 'create'])
->name('support-requests.create');
Route::post('/support/requests', [SupportRequestController::class, 'store'])
->name('support-requests.store');
});
Nie przenoś każdej asercji renderowania do prawdziwej przeglądarki. Niepoprawne przypadki requestu i odpowiedzi kontrolera testuj w tests/Feature. Ten test zachowaj dla kontraktu przechodzącego przez JavaScript, nawigację i UI.
Twórz cały stan jawnie
Przerywany test przeglądarkowy jest zazwyczaj problemem stanu przebranym za problem timingu. Resetuj bazę, twórz aktora i dane referencyjne zamiast zakładać, że istnieje lokalny seed. RefreshDatabase izoluje każdy przykład, a factories czynią przygotowanie oczywistym.
<?php
declare(strict_types=1);
use App\Models\SupportCategory;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
uses(RefreshDatabase::class);
beforeEach(function (): void {
Http::preventStrayRequests();
$this->customer = User::factory()->create([
'email' => '[email protected]',
]);
$this->billingCategory = SupportCategory::factory()->create([
'name' => 'Billing',
'is_active' => true,
]);
});
Http::preventStrayRequests() jest wartościowe, nawet gdy strona dziś nie ma zdalnego wywołania. Przyszły beacon analityczny, lookup adresu lub pomocnik AI powinien od razu zepsuć test, nie łączyć się z internetem z CI. Jeśli ścieżka celowo wywołuje integrację, sfake’uj jej dokładną odpowiedź. Pytamy „czy nasz produkt zadziałał?”, nie „czy dostawca był dziś dostępny?”.
Unikaj wspólnych kont, istniejącego pliku SQLite i SupportCategory::first(). Ukrywają warunki wstępne. Przewidywalne daty, UUID-y i joby kolejki wymagają tego samego: zamrożenia lub fake’a w warstwie, która nimi zarządza.
Wybieraj kontrolki jak użytkownik
Dostępna nazwa jest zwykle najtrwalszym selektorem. Potrzebuje jej także osoba z czytnikiem ekranu, więc test wzmacnia prawdziwy wymóg. Billing oraz przycisk Send request opisują produkt. .grid > div:nth-child(2) button opisuje chwilową implementację.
Poniżej pełny test pozytywnej ścieżki. Należy do tests/Browser; stronę odwiedzamy dopiero po utworzeniu użytkownika, kategorii i polityki HTTP.
<?php
declare(strict_types=1);
use App\Models\SupportRequest;
it('lets a customer submit a support request', function (): void {
$this->actingAs($this->customer);
$page = visit(route('support-requests.create'));
$page->assertSee('New support request')
->assertNoJavaScriptErrors()
->fill('Subject', 'Invoice contains the wrong company name')
->select('Category', $this->billingCategory->name)
->fill('Message', 'Please correct the name before the payment deadline.')
->click('Send request')
->waitForText('Your request has been sent')
->assertNoJavaScriptErrors();
expect(SupportRequest::query()->where([
'user_id' => $this->customer->id,
'subject' => 'Invoice contains the wrong company name',
'support_category_id' => $this->billingCategory->id,
])->exists())->toBeTrue();
});
Asercja po nawigacji jest celowa. Wyjątek przy pierwszym renderowaniu nie jest jedynym trybem awarii: uszkodzony toast albo krok hydracji może pojawić się po POST. Sprawdzenie konsoli na obu końcach zawęża okno diagnozy.
Gdy kontrolka nie ma użytecznej etykiety, napraw UI, zamiast używać kruchego selektora. To pole Reacta daje użytkownikom i testom stabilny kontrakt:
<label htmlFor="support-subject">Subject</label>
<input
id="support-subject"
name="subject"
required
value={data.subject}
onChange={(event) => setData('subject', event.target.value)}
/>
<button type="submit" disabled={processing}>
{processing ? 'Sending request…' : 'Send request'}
</button>
data-testid stosuj wyłącznie tam, gdzie nie istnieje sensowna dostępna nazwa, na przykład dla dekoracyjnego canvasu mapy. To nazwana furtka, nie pozwolenie na oznaczanie każdego div.
Czekaj na wynik, który widzi klient
sleep(2) to zakład, że współdzielony runner CI będzie wystarczająco szybki. Traci czas w szybkim uruchomieniu i flakuje w wolnym. Czekaj na obserwowalny wynik: nagłówek, toast, znikający loader albo aktywną kontrolkę. To także wartościowe zobowiązanie UX.
<?php
declare(strict_types=1);
it('shows completion after importing a CSV', function (): void {
$this->actingAs($this->customer);
$page = visit('/imports/create');
$page->attach('CSV file', base_path('tests/Fixtures/customers.csv'))
->click('Import customers')
->waitForText('Import complete')
->assertNoJavaScriptErrors();
});
Jeśli zakończenie posiada kolejka, skonfiguruj tu synchroniczną kolejkę lub sfake’uj granicę usługi. Test UI nie może zależeć od tego, czy osobny worker akurat odbierze job. Prawdziwy test wdrożeniowy wieloprocesowego systemu należy do wydzielonego smoke suite na stagingu, z własnym timeoutem i diagnozą.
Pokryj jedną awarię, z której można się podnieść
Pozytywna ścieżka łapie luki integracyjne; jeden reprezentatywny błędny przypadek dowodzi, że interfejs pozostaje zrozumiały. Nie zastępuje tabeli testów feature pokrywającej każdą regułę walidacji.
<?php
declare(strict_types=1);
use App\Models\SupportRequest;
it('keeps the form visible when the message is missing', function (): void {
$this->actingAs($this->customer);
$page = visit(route('support-requests.create'));
$page->fill('Subject', 'Question about my invoice')
->select('Category', $this->billingCategory->name)
->click('Send request')
->waitForText('The message field is required.')
->assertSee('New support request')
->assertNoJavaScriptErrors();
expect(SupportRequest::query()->count())->toBe(0);
});
Nie asercjuj całego układu wizualnego ani każdego tłumaczonego zdania. Ważne ścieżki padałyby wtedy przez niepowiązaną zmianę copy lub odstępów. Sprawdź stan potrzebny klientowi: błąd pokazany, formularz zachowany, rekord nieutworzony.
Świadomie wspieraj przeglądarki w CI
Testy wymagają serwera aplikacji, wspieranej binarki przeglądarki i zależności systemowych. Przypnij wersje PHP, Node oraz przeglądarki w CI, zainstaluj ją w obrazie joba i izoluj URL aplikacji oraz bazę dla każdego uruchomienia. Zacznij sekwencyjnie: współdzielone porty, katalogi pobierania i bazy produkują mylące błędy, jeśli równoległość zostanie dodana za wcześnie.
Zapisuj na awarii screenshoty, wyjście konsoli i trace przeglądarki albo snapshot HTML. Wyjście testu powie, że waitForText przekroczył czas; artefakt pokaże, czy klient zobaczył przekierowanie do logowania, błąd serwera czy niewidoczną nakładkę. Mały zestaw browserowy uruchamiaj po szybkich testach: analiza i formatowanie, unit/feature równolegle, potem przeglądarka na czystej bazie. Wiele przeglądarek lub viewportów dodawaj tylko tam, gdzie daje to zaufanie, na przykład dla mobilnego widgetu płatności.
Pułapki, alternatywy i granice
Typowe błędy są przewidywalne: selektory zależne od CSS czynią redesigny kosztownymi; prawdziwe zewnętrzne API tworzą flaky testy i mogą ujawnić dane; arbitralne opóźnienia ukrywają wyścigi; wspólne seedy pozwalają przejść testom przypadkiem; ignorowane ostrzeżenia konsoli kryją niedoładowane chunki, błędy hydracji i nieobsłużone Promise. Brak binarki Chromium, kolizja portu albo niedostępna baza to problem harnessu, nie defekt aplikacji — najpierw napraw obraz joba.
Nie uruchamiaj prawdziwej przeglądarki dla czystego wyliczenia ceny, decyzji polityki, resource JSON ani każdej gałęzi Form Request. Testy unit czynią reguły domenowe tanimi w czytaniu, a feature szybko ćwiczą trasy, middleware, walidację i zapis. Browser tests są ostatnią, selektywną warstwą. Dla szerokiej regresji lepszy może być smoke test krótkiej listy stron bez błędów JavaScriptu. Porównanie screenshotów jest osobnym narzędziem regresji wizualnej z recenzowanym baseline'em. Dla hostowanego checkoutu dostawcy sfake’uj providera w CI, a mały contract check zostaw stagingowi i odseparowanemu kontu.
W rezultacie garść testów przeglądarkowych pada z użytecznych powodów: chroni ścieżki, za których działanie płacą użytkownicy, a reszta piramidy testów pozostaje dostatecznie szybka dla każdej zmiany.