Blog
Przemyślana strategia testów w Symfony i React: szybki feedback, stabilność i koszt utrzymania
Jak dobierać testy Symfony i React do ryzyka, skracać czas informacji zwrotnej i bezpiecznie refaktoryzować kod bez budowania kosztownego, kruchego zestawu testów.
Testy mają pomagać w zmienianiu aplikacji
Wyobraźmy sobie zmianę w edytorze artykułów: szkic można zapisać bez tytułu, ale publikacja wymaga kompletnej treści. Symfony egzekwuje regułę, a React wyjaśnia ją użytkownikowi. Uruchamianie całego zestawu testów przeglądarkowych po każdej poprawce w końcu pokaże, czy proces się zepsuł. To jednak kosztowny sposób wykrywania błędu w jednym warunku polityki.
Równie drogi bywa przeciwny skrót: sprawdzić warunek w izolacji i uznać, że edytor, API oraz baza na pewno współpracują poprawnie. Przemyślana strategia przypisuje ryzykom odpowiednie testy i pozostawia wystarczającą ochronę na styku warstw.
Korzyść jest konkretna: mniej przerw podczas pracy, czytelniejsze błędy przy refaktoryzacji i mniej regresji docierających do użytkowników. Zostaje jednak rachunek za utrzymanie. Testy wymagają danych, środowisk i przeglądu, a wyjaśnianie ich porażek zajmuje czas. Ten artykuł dotyczy ograniczania tego kosztu przy zachowaniu użytecznej pewności zmian. Przykłady funkcji są hipotetyczne, nie opisują projektów klientów GiSoft.
Szybkość informacji zwrotnej wynika z architektury
Warto mierzyć czas od wprowadzenia zmiany do zrozumienia, czy jest bezpieczna. Samo wykonanie testów to tylko część tej drogi. Kolejka do runnera CI, przygotowanie bazy i analiza niejasnego błędu wydłużają ją równie skutecznie. Jeśli wynik przychodzi po przejściu do innego zadania, trzeba jeszcze odtworzyć kontekst.
Zmiana obliczenia powinna zwykle szybko otrzymać odpowiedź z analizy statycznej i wąskich testów jednostkowych. Zmiana repozytorium wymaga właściwych testów integracyjnych z bazą. Test przeglądarkowy może następnie sprawdzić dotknięty proces. Uruchamiając wszystko od razu, płacimy najwyższy koszt przygotowania, zanim zadamy najprostsze pytania.
Mierz te koszty we własnym repozytorium. Analiza statyczna nie zawsze jest szybka, a mały test integracyjny może kosztować mniej niż test jednostkowy obudowany wieloma mockami. Obserwuj czas do pierwszego błędu pozwalającego podjąć działanie oraz czas całego pipeline’u. Krótszy test ma wartość wtedy, gdy wykrywa istotny problem.
Dobieraj test do tego, co może zawieść
Nazwy zestawów różnią się między zespołami. Praktyczniejszy podział dotyczy tego, które rzeczywiste elementy wykonujemy, a które zastępujemy. Zacznij od konkretnego błędu i wybierz najtańszy test, który rzeczywiście potrafi go zaobserwować:
Możliwy błąd Pierwsza użyteczna granica
Złe obliczenie biznesowe Jednostkowy: wejście i wynik
Błędne zapytanie Doctrine Integracyjny: baza testowa
Brak uprawnienia w API Funkcjonalny: żądanie i efekt
Utrata wpisu po błędzie UI Komponent: interakcja użytkownika
Zmiana formatu odpowiedzi Kontrakt: producent i odbiorca
Zepsuty kompletny proces Przeglądarka: wybrana ścieżka
Niedostępne wdrożenie Smoke: wdrożony punkt wejściaTo punkt wyjścia, nie sztywny przydział. Polityka uprawnień może mieć wiele testów jednostkowych oraz test HTTP potwierdzający, że trasa jej używa. Wyższe poziomy zwiększają pewność połączeń; nie muszą powtarzać każdego wariantu niżej.
Kontrole statyczne warto uruchamiać wcześnie. PHPStan i TypeScript wykrywają niezgodne typy, niebezpieczne założenia o null i naruszenia zadeklarowanych interfejsów bez odtwarzania procesu użytkownika. Linting i jawne reguły importów pomagają wykrywać część błędów na granicy klient–serwer. Testy architektury mogą egzekwować uzgodniony kierunek zależności, np. brak typów HTTP w politykach biznesowych. Sprawdzają jednak tylko analizowany kod i skonfigurowane reguły; nie dowodzą zachowania biznesowego podczas wykonania.
W Symfony testuj decyzje blisko kodu, a infrastrukturę rzeczywiście
Testy jednostkowe pasują do obiektów wartości, obliczeń, polityk, mapperów i własnych reguł walidacji. Zwykły obiekt daje krótką drogę od danych wejściowych do przyczyny błędu. Zwykle nie potrzebuje kernela ani bazy.
Poniższy test Pest sprawdza przykładową klasę PublishPostPolicy. Zakładana metoda przyjmuje tytuł i treść oraz odrzuca publikację, jeśli po usunięciu białych znaków z brzegów którekolwiek pole jest puste. Implementację i import pominięto; to nie API Symfony ani istniejący helper projektu. Składnia odpowiada używanemu w repozytorium zestawowi Pest 3/PHPUnit 11:
it('wymaga treści przed publikacją', function (
string $title,
string $body,
bool $allowed,
): void {
$policy = new PublishPostPolicy();
expect($policy->canPublish(title: $title, body: $body))
->toBe($allowed);
})->with([
['', 'Article text', false],
['Title', '', false],
[' ', 'Article text', false],
['Title', 'Article text', true],
]);Poprawny przypadek ma znaczenie: bez niego polityka odrzucająca każdą publikację również przeszłaby test. Sprawdzamy gotowość treści, a nie uprawnienie klienta. Autoryzacja pozostaje osobnym obowiązkiem serwera.
Testy integracyjne uzasadniają swój koszt, gdy zawieść może sama granica: mapowanie Doctrine i DQL, połączenia usług w kontenerze, konfiguracja serializera, adapter cache czy routing Messengera. Uruchom wtedy właściwą rzeczywistą infrastrukturę. Mock buildera zapytań nie pokaże, czy złączenia powielają artykuły. Korzystaj z odizolowanej bazy testowej o odpowiednim silniku i schemacie; priorytet daj zapytaniom, których błędy mają konsekwencje.
Testy funkcjonalne API przechodzą przez Symfony i sprawdzają routing, metody, poświadczenia, autoryzację, dekodowanie, walidację, odpowiedź publiczną oraz wybrane efekty. Przy publikacji ustal trzy wyniki: anonimowy dostęp jest odrzucany według kontraktu API, rozpoznany klient bez uprawnienia nie zmienia rekordu, a uprawniony może go opublikować. Nie wnioskuj o uwierzytelnieniu wyłącznie z 403; konkretna odpowiedź zależy od entry pointu i konfiguracji. Klient testowy Symfony nie sprawdza wdrożonego proxy ani prawdziwej przeglądarki.
Artykuł GiSoft Jak testować API Symfony za pomocą Pest rozwija te przykłady HTTP. Tutaj chodzi o decyzję strategiczną: tanio sprawdzić regułę, a następnie potwierdzić, że endpoint ją stosuje. Test serializera lub repozytorium nie zastąpi tego połączenia.
W React sprawdzaj działanie i możliwość powrotu po błędzie
Czyste funkcje, reducery, formatowanie i przejścia stanu również nadają się do testów jednostkowych. Testy komponentów zyskują wartość, gdy zachowanie zależy od renderowania i interakcji: komunikatów walidacji, ładowania, blokady przycisków, obsługi błędu i powodzenia operacji.
Repozytorium używa już Vitest, React Testing Library i user-event, z matcherami jest-dom w konfiguracji testów. Fragment zakłada przykładowy DraftEditor, którego implementacji artykuł nie dostarcza. Komponent przyjmuje initialTitle i asynchroniczne saveDraft({ title }) oraz wyświetla użyte tutaj angielskie etykiety:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, it, vi } from 'vitest';
import { DraftEditor } from './DraftEditor';
it('zachowuje zmieniony tekst po błędzie zapisu', async () => {
const user = userEvent.setup();
const saveDraft = vi.fn().mockRejectedValue(new Error('Unavailable'));
render(<DraftEditor initialTitle="Release notes" saveDraft={saveDraft} />);
const title = screen.getByRole('textbox', { name: 'Title' });
await user.clear(title);
await user.type(title, 'Revised notes');
await user.click(screen.getByRole('button', { name: 'Save draft' }));
expect(await screen.findByRole('alert'))
.toHaveTextContent('Could not save');
expect(screen.getByRole('textbox', { name: 'Title' }))
.toHaveValue('Revised notes');
expect(screen.getByRole('button', { name: 'Save draft' })).toBeEnabled();
expect(saveDraft).toHaveBeenCalledWith({ title: 'Revised notes' });
});Test sprawdza konkretną obietnicę: po nieudanym zapisie użytkownik nadal ma zmieniony tekst i może ponowić próbę. Nie odczytuje stanu hooka, prywatnego helpera ani zagnieżdżenia komponentów. Wyszukiwanie po roli i nazwie dostępnej dla technologii wspomagających zwykle lepiej wyraża intencję niż selektory CSS zależne od układu. Samo w sobie nie potwierdza pełnej dostępności.
Stan ładowania sprawdź osobno, korzystając z kontrolowanego, jeszcze nierozstrzygniętego Promise, który potem jawnie zakończysz. Wyłączony przycisk może zatrzymać kolejne kliknięcie w tym interfejsie; nie dowodzi idempotencji backendu. Ukrycie przycisku publikacji nie egzekwuje autoryzacji serwera. Snapshot całej strony często zaciemnia te różnice, choć mały snapshot ma sens, gdy to właśnie wynik renderowania jest kontraktem.
Sprawdź umowę między Symfony i Next.js
Testy kontraktowe sprawdzają zgodność producenta i odbiorcy danych: statusy, pola wymagane i dopuszczające null, formaty dat, kody błędów, paginację oraz założenia uwierzytelniania. Mogą korzystać ze schematu, weryfikacji producenta albo przykładów odpowiedzi sprawdzanych po obu stronach. Fixture frontendu wymyślona niezależnie od backendu potwierdza jedynie zgodność z samą sobą.
TypeScript opisuje oczekiwania kompilowanego kodu. Rzutowanie zdekodowanego JSON na interfejs nie waliduje danych. Na granicy adaptera API sprawdź poprawną odpowiedź i istotny przypadek niezgodności, używając przyjętej w projekcie walidacji podczas wykonania. Jeśli Symfony zmieni publishedAt z ciągu ISO na liczbę, odbiorca powinien obsłużyć uzgodnioną zmianę albo przewidywalnie ją odrzucić. Sam schemat nie wykryje odpowiedzi o poprawnym kształcie zawierającej dane innego użytkownika.
Next.js dokłada granicę środowiska wykonania. Nawigacja po stronie klienta, pełne odświeżenie i renderowanie serwerowe mogą inaczej pozyskiwać poświadczenia oraz dane z cache. Serwerowy fetch nie dziedziczy automatycznie cookies ani nagłówka Authorization z żądania przeglądarki. Sprawdź zaprojektowane przekazanie poświadczeń, bez kopiowania dowolnych nagłówków. Czystą logikę mapowania utrzymuj łatwą do testowania, a zachowanie serwerowe weryfikuj w odpowiednim środowisku Next.js.
Możliwości testowania Server Components zależą od narzędzi i ich wersji. Przewodnik Next.js 16 dla Vitest wskazuje brak obsługi asynchronicznych Server Components i zaleca dla nich testy E2E. Przechodzący test komponentu klienckiego nie mówi więc nic o tej ścieżce serwerowej. Osobny artykuł GiSoft Jak testować architekturę, błędy i procesy użytkownika w Next.js szerzej opisuje te granice wykonania i obsługi awarii.
Zostaw przeglądarce połączenia, które mają znaczenie
Testy przeglądarkowe/E2E mogą potwierdzić współpracę nawigacji, renderowania, poświadczeń i backendu. Mają jednak szeroki obszar diagnozy: przyczyną jednego błędu bywa selektor, wygasła sesja, brak danych, sieć albo sama usługa. Najwięcej wnoszą przy krytycznej ścieżce zakupu, logowania czy publikacji oraz wybranym przypadku odzyskania działania po kosztownej awarii.
Używaj runnera przeglądarkowego przyjętego w projekcie. Playwright jest jedną z możliwości, ale manifest pakietów tego frontendu nie deklaruje obecnie skonfigurowanego zestawu Playwright. Przykład z artykułu nie powinien wymuszać drugiego stosu testowego.
Jawnie określ zastępowane elementy. Test przeglądarkowy z mockami wszystkich odpowiedzi API sprawdza proces w przeglądarce, nie rzeczywistą integrację z Symfony. Połącz przynajmniej istotną ścieżkę krytyczną z kontrolowanymi danymi backendu. W zwykłym CI używaj atrap dostawców, a celowe sprawdzenia ich środowisk testowych prowadź osobno. Prawdziwe płatności i niekontrolowane usługi zewnętrzne nie należą do rutynowych uruchomień.
Testy smoke odpowiadają na węższe pytanie o wdrożenie: czy punkt wejścia odpowiada i czy wybrany bezpieczny odczyt lub kontrola istotnej zależności kończy się powodzeniem? Uzupełniają zestaw aplikacyjny i monitoring. Nie potwierdzają wszystkich uprawnień ani reguł biznesowych.
Limity regresji mogą chronić istotny endpoint listy przed wzrostem liczby zapytań, nadmiernym rozmiarem odpowiedzi lub pomijaniem limitu paginacji. Wyznacz progi na reprezentatywnych danych i oddziel przygotowanie testu od pomiaru. Ograniczona liczba zapytań nie jest benchmarkiem opóźnienia. Szczegółowe badanie N+1 i wydajności zostaw ukierunkowanym testom oraz profilowaniu, zamiast mierzyć czas w każdym teście przeglądarkowym.
Projektuj CI pod użyteczne wyniki
Rozsądny pipeline zaczyna od składni, typów i innych niedrogich kontroli, potem obejmuje wąskie testy jednostkowe i komponentowe, integrację, API oraz wybrane ścieżki przeglądarkowe. Niezależne zadania mogą działać równolegle. Niektóre testy integracyjne są na tyle tanie, że warto uruchamiać je od razu; zmiana współdzielonej biblioteki może od początku wymagać szerszej ochrony.
Obserwuj czas oczekiwania i łączne zużycie runnerów. Pełna równoległość może skrócić oczekiwanie, a jednocześnie podnieść koszty infrastruktury lub zwiększyć rywalizację o wspólną bazę. W razie potrzeby rozdziel dane, przestrzenie nazw cache i porty procesów. Cache zależności powiąż z odpowiednimi lockfile’ami; nie przyspieszaj testów przez ponowne używanie ich mutowalnego stanu.
Podczas pracy uruchamiaj testy blisko zmiany. Przed scaleniem dobierz zakres do jej wpływu: wybór wyłącznie po nazwach zmienionych plików może pominąć skutki we wspólnym kodzie, schematach i konfiguracji. Zachowaj szerszą kontrolę na odpowiednim etapie. Przy błędach przeglądarkowych zbieraj ślady wykonania i przydatne logi bez sekretów. Powtórzenie testu może pomóc zbadać niestabilność; nie powinno po cichu zamieniać niewiarygodnego wyniku w wiarygodny.
Mierz kolejkę, przygotowanie środowiska, wolne zestawy, błędy występujące nieregularnie i czas analizy. Popraw to, co rzeczywiście zabiera czas, zanim dokupisz runnery albo usuniesz wartościowe asercje. Nie istnieje uniwersalny czas CI ani proporcja testów jednostkowych do E2E świadcząca o dobrej strategii.
Koszt utrzymania jest częścią projektu
Test tworzy wartość, gdy zawodzi przy istotnej zmianie i pomaga wyjaśnić problem. Powtarzające się awarie spowodowane niepowiązanym HTML lub współdzielonymi danymi zabierają czas i uczą ignorowania wyników. Ten koszt należy uwzględniać przy projektowaniu, nie odkładać wyłącznie na późniejsze porządki.
Dane testowe powinny być małe i jawnie określać istotny stan: właściciela, rolę, publikację i język. Używaj deterministycznych identyfikatorów oraz wstrzykniętego zegara tam, gdzie czas wpływa na wynik. Izoluj zmienny stan i unikaj zależności od kolejności wykonania. Helper ma usuwać powtórzenia bez ukrywania uwierzytelniania, zapisów w bazie i powodu oczekiwanego sukcesu.
Zastępuj zależność, gdy potrzebujesz jej kontrolowanej odpowiedzi. Pozostaw rzeczywistą, jeśli ryzyko dotyczy jej zachowania. Nadmiar mocków pozwala przejść testowi usługi mimo błędnych połączeń w kontenerze czy zapisu w produkcji. Wiąże też testy z kolejnością wywołań i strukturą współpracujących obiektów. Preferuj obserwowalny wynik; liczbę wewnętrznych wywołań sprawdzaj wtedy, gdy sama interakcja jest wymaganiem.
Nie kupuj wielokrotnie tego samego dowodu. Przy obliczaniu ceny testy jednostkowe sprawdzają zaokrąglenia i wartości graniczne, API — mapowanie kwoty i waluty na odpowiedź, a przeglądarka — możliwość zakończenia zakupu. Powtarzanie każdego przypadku zaokrąglenia przez przeglądarkę zwiększa koszty przygotowania i diagnozy, niewiele dodając do ochrony połączeń.
Testy stabilnego zachowania powinny w większości przetrwać refaktoryzację klas czy kompozycji komponentów. Przy celowej zmianie zachowania aktualizuj kontrakt i testy razem. Zbędny test usuń lub przenieś po wskazaniu, co nadal chroni dane ryzyko. Niestabilny test potrzebuje właściciela i terminu naprawy; wyłączenie go bezterminowo jedynie ukrywa lukę.
Zastosuj strategię do jednej funkcji publikacji
Pierwszy plan dla hipotetycznego edytora może być niewielki:
- Polityka: jednostkowe przypadki brakującej treści, białych znaków i poprawnego artykułu. HTTP nie jest potrzebne.
- Dane i dostęp: test integracyjny wyboru właściwego artykułu i tłumaczenia; API sprawdza odmowę bez zmiany oraz dozwoloną publikację z zapisanym wynikiem.
- Edytor: komponent sprawdza oczekiwanie, walidację, zachowanie wpisu po błędzie i sukces. Test adaptera weryfikuje mapowanie odpowiedzi i błędów na te stany.
- Połączony proces: przeglądarka loguje użytkownika, edytuje i publikuje, a świeży odczyt potwierdza faktyczny wynik. Uwzględnij pełne odświeżenie, jeśli ważne jest renderowanie serwerowe; osobną ścieżkę naprawczą dodaj dla odrębnego ryzyka.
- Wdrożenie: bezpieczny smoke test sprawdza dostępność wdrożonej trasy. Test cache lub budżetu zapytań dodaj, jeśli funkcja wnosi właśnie takie ryzyko.
W dojrzałej aplikacji zacznij od zachowania, które zamierzasz zmienić. W razie potrzeby zabezpiecz niechronioną ścieżkę krytyczną, a szczegółowe warianty przenoś bliżej reguły w miarę poznawania kodu. Nie musisz najpierw przepisywać całego zestawu ani osiągać arbitralnego procentu pokrycia.
Następny test powinien rozstrzygać nazwaną niewiadomą. Umieść go tam, gdzie można to zrobić wiarygodnie, a efekt oceniaj po szybkości wprowadzania i diagnozowania zmian, nie po liczbie dopisanych testów.
Źródła techniczne
- Testowanie w Symfony 7.4: https://symfony.com/doc/7.4/testing.html
- Interakcje użytkownika w Testing Library: https://testing-library.com/docs/user-event/intro/
- Testy Next.js z Vitest i aktualne ograniczenia: https://nextjs.org/docs/app/guides/testing/vitest
- Asercje typów TypeScript a wykonanie programu: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions
