Blog
Jak testować architekturę, błędy i procesy użytkownika w Next.js
Niezawodne testowanie Next.js nie ogranicza się do snapshotów komponentów i poprawnych odpowiedzi API. Strategia produkcyjna musi chronić granice architektury, obsługę awarii, zachowanie części serwerowej i klienckiej oraz kompletne procesy użytkowników.
Przechodzący test komponentu nie dowodzi, że aplikacja działa
Aplikacja Next.js może mieć setki przechodzących testów jednostkowych, a mimo to zawieść w procesie, który ma znaczenie dla klienta.
Przycisk może renderować się poprawnie, podczas gdy:
- serwer zwraca dane w nieoczekiwanym kształcie,
- sesja wygasa podczas wysyłania formularza,
- trasa zgłasza wyjątek w trakcie renderowania,
- po mutacji w cache pozostają nieaktualne dane,
- żądanie jest wysyłane dwukrotnie,
- przekierowanie gubi pierwotny adres docelowy,
- error boundary ukrywa przydatne możliwości odzyskania działania,
- API dostępne tylko w przeglądarce zawodzi podczas renderowania po stronie serwera,
- stan ładowania nigdy się nie kończy,
- aktualizacja optymistyczna pokazuje dane, które nie zostały zapisane.
Strategia testowania musi więc chronić więcej niż komponenty.
Produkcyjna aplikacja Next.js powinna być testowana w trzech powiązanych wymiarach:
Architektura
→ Czy odpowiedzialności i kierunki zależności są właściwe?
Awarie
→ Czy aplikacja zachowuje się bezpiecznie, gdy zawodzą jej zależności?
Procesy użytkownika
→ Czy użytkownik może osiągnąć ważny cel przez rzeczywisty interfejs?Testowanie tych wymiarów oddzielnie jest użyteczne.
Dopiero testowanie ich wzajemnych zależności rzeczywiście chroni aplikację.
Zacznij od zachowania, od którego zależy biznes
Zespoły często zaczynają od pytania:
Jaki procent pokrycia testami powinniśmy osiągnąć?Lepsze pytanie brzmi:
Które awarie uniemożliwiłyby użytkownikowi ukończenie ważnego zadania?Rozważmy proces składania zamówienia w sklepie internetowym:
Otwórz produkt
→ dodaj produkt do koszyka
→ otwórz checkout
→ wprowadź dane dostawy
→ złóż zamówienie
→ autoryzuj płatność
→ wyświetl potwierdzenieDla biznesu nie ma znaczenia, czy każdy komponent React ma osobny snapshot.
Znaczenie ma to, czy:
- właściwy produkt trafia do koszyka,
- przeglądarka nie może zmienić cen,
- zamówienie nie zostaje złożone dwukrotnie,
- po błędzie płatności można kontynuować,
- wygaśnięcie sesji nie usuwa wszystkich wprowadzonych danych,
- udana płatność tworzy dokładnie jedno zamówienie,
- potwierdzenie pokazuje właściwy numer zamówienia.
To proces powinien wyznaczać strategię testowania.
Użyteczna mapa testów aplikacji Next.js
Praktyczna architektura testów może wyglądać następująco:
Kontrole statyczne
│
├── TypeScript
├── linting
└── reguły importów i architektury
│
▼
Testy domeny i warstwy aplikacyjnej
│
├── obliczenia biznesowe
├── przejścia między stanami
└── orkiestracja przypadków użycia
│
▼
Testy komponentów
│
├── interakcje
├── walidacja
├── ładowanie
└── błędy możliwe do obsłużenia
│
▼
Testy kontraktowe i integracyjne
│
├── odpowiedzi backendu
├── uwierzytelnianie
├── route handlers
└── unieważnianie cache
│
▼
Testy procesów w przeglądarce
│
├── nawigacja
├── kompletne formularze
├── przekierowania
└── krytyczne ścieżki
│
▼
Weryfikacja produkcyjna
├── logi
├── metryki
├── trace’y
└── testy syntetyczneKażda warstwa odpowiada na inne pytanie.
Używanie wyłącznie testów przeglądarkowych sprawia, że diagnozowanie awarii jest powolne.
Używanie wyłącznie testów jednostkowych pozostawia błędy integracyjne bez ochrony.
Pipeline CI powinien przy każdej zmianie uruchamiać szybkie, deterministyczne kontrole, izolować dane testów przeglądarkowych i traktować wolniejsze procesy między usługami jako zależne od ryzyka bramki wydania, a nie opcjonalną diagnostykę.
Testuj architekturę, zanim stanie się trudna do zmiany
Testy architektury weryfikują, od czego kod może zależeć.
Utrzymywalny projekt Next.js może stosować granice podobne do tych:
app
→ kompozycja tras i integracja z frameworkiem
features
→ przypadki użycia widoczne dla użytkownika
domain
→ typy i reguły biznesowe
application
→ orkiestracja i porty
infrastructure
→ klienty API, storage i adaptery dostawców
ui
→ współdzielone komponenty prezentacyjneDokładne nazwy katalogów są mniej ważne niż kierunek zależności.
Funkcja nie powinna wymagać, aby komponent React znał:
- surowe struktury bazy danych,
- obiekty odpowiedzi SDK dostawcy,
- wewnętrzną strukturę ciasteczek uwierzytelniających,
- kody błędów operatora płatności,
- szczegóły implementacji cache.
Koncepcyjna reguła zależności wygląda tak:
UI
→ kontrakt warstwy aplikacyjnej
→ kontrakt domenowy
← adapter infrastrukturyInfrastruktura implementuje kontrakt.
Warstwa aplikacyjna nie zależy od dostawcy.
Przykład: odizoluj zależność checkoutu
Zamiast wywoływać zewnętrzne API bezpośrednio z komponentu:
export function CheckoutForm(): JSX.Element {
async function submit(): Promise {
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(formValues),
});
}
return {/* ... */};
}zdefiniuj kontrakt należący do projektu:
export type CheckoutInput = Readonly;
export type CheckoutResult =
| Readonly
| Readonly
| Readonly;
export interface CheckoutGateway {
submit(input: CheckoutInput): Promise;
}Adapter infrastruktury jest właścicielem szczegółów HTTP:
export class HttpCheckoutGateway implements CheckoutGateway {
public constructor(
private readonly baseUrl: string,
private readonly fetcher: typeof fetch,
) {}
public async submit(
input: CheckoutInput,
): Promise {
const response = await this.fetcher(
`${this.baseUrl}/orders`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': input.idempotencyKey,
},
body: JSON.stringify(input),
},
);
if (!response.ok) {
throw await mapCheckoutFailure(response);
}
return validateCheckoutResult(await response.json());
}
}Testy mogą teraz zastąpić gateway bez globalnego podmieniania komunikacji sieciowej przeglądarki.
Adapter może mieć skupione testy kontraktowe.
Komponent może mieć testy zachowania.
Pełny proces nadal może zostać zabezpieczony testem przeglądarkowym.
Testy architektury powinny wymuszać rzeczywiste granice
W zależności od narzędzi używanych już w projekcie reguły architektury można sprawdzać przez:
- ograniczenia importów w lintingu,
- analizę zależności,
- referencje projektów TypeScript,
- granice pakietów,
- ukierunkowane testy kodu źródłowego.
Użyteczne reguły obejmują:
Kod domenowy nie może importować React ani Next.js.
Przypadki użycia warstwy aplikacyjnej nie mogą importować SDK dostawców.
Komponenty UI nie mogą bezpośrednio importować implementacji infrastruktury.
Moduł funkcjonalny nie może sięgać do wewnętrznych plików innego modułu.
Moduły server-only nie mogą trafiać do bundli klienckich.
Pliki tras powinny składać zależności, a nie zawierać kompletne procesy biznesowe.Nie twórz reguł tylko dlatego, że brzmią elegancko.
Każda reguła powinna zapobiegać rzeczywistej klasie błędów.
Testuj obserwowalne zachowanie, a nie wewnętrzną implementację
Kruchy test komponentu sprawdza:
Komponent wywołał setState trzy razy.Użyteczny test sprawdza:
Przycisk wysyłania jest niedostępny w trakcie żądania.
Nieudane żądanie pokazuje błąd, po którym można kontynuować.
Wartości wprowadzone przez użytkownika pozostają w formularzu.
Udany wynik prowadzi do strony potwierdzenia.Testy powinny przetrwać wewnętrzną refaktoryzację, jeśli zachowanie widoczne dla użytkownika się nie zmieniło.
Unikaj sprawdzania:
- wywołań prywatnych helperów,
- szczegółów implementacji hooków,
- dokładnej głębokości drzewa komponentów,
- pełnych snapshotów HTML złożonych ekranów,
- klas generowanych przez framework.
Sprawdzaj rezultaty i stabilne role dostępności.
Testuj odpowiedzialności serwera i klienta oddzielnie
Aplikacje Next.js często łączą dane renderowane na serwerze z interakcjami po stronie klienta.
Użyteczny podział wygląda tak:
Odpowiedzialność serwera
→ uwierzytelnianie
→ początkowe ładowanie danych
→ autoryzacja
→ stabilna kompozycja strony
Odpowiedzialność klienta
→ natychmiastowa interakcja
→ lokalna walidacja
→ stan oczekiwania
→ ponowienie
→ progresywne aktualizacjeTest serwera powinien potwierdzać, że nieuprawnieni użytkownicy nie otrzymują chronionych danych.
Test komponentu powinien potwierdzać, że użytkownik widzi użyteczny stan, gdy mutacja po stronie klienta kończy się błędem.
Test przeglądarkowy powinien sprawdzać pełne zachowanie.
Nie wymuszaj testowania każdego warunku kosztownym testem przeglądarkowym.
Przypadki awarii muszą być częścią projektu
Większość testów zaczyna się od poprawnych odpowiedzi:
gateway.submit.mockResolvedValue({
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-2026-123',
});Incydenty produkcyjne zwykle zaczynają się gdzie indziej:
API przekroczyło limit czasu.
Użytkownik kliknął dwukrotnie.
Odpowiedź była poprawnym JSON-em, ale miała nieprawidłowy kształt.
Sesja wygasła.
Koszyk się zmienił.
Dostawca płatności zaakceptował płatność, lecz przekierowanie nie zadziałało.
Cache pokazał stare dane.Dla procesu należy przygotować jawną macierz awarii.
Przykładowa macierz awarii checkoutu
Awaria Oczekiwane zachowanie aplikacji
─────────────────────────────────────────────────────────────────────────────
Żądanie produktu zawodzi Pokaż ponowienie bez awarii całej trasy
Koszyk jest pusty Przekieruj albo pokaż czytelny pusty stan
Koszyk zmienił się na serwerze Odśwież koszyk i poproś o potwierdzenie
Sesja wygasła Poproś o logowanie i zachowaj adres powrotu
Checkout przekracza czas Pokaż stan niepewny; nie ponawiaj automatycznie
Płatność odrzucona Zachowaj kontekst i pozwól wybrać inną metodę
Nieprawidłowy schemat odpowiedzi Pokaż bezpieczny błąd; zgłoś naruszenie kontraktu
Podwójne wysłanie Użyj tego samego klucza i nie duplikuj zamówienia
Żądanie potwierdzenia zawodzi Pozwól odzyskać potwierdzenie po numerze zamówienia
Nieaktualny koszyk w cache Unieważnij lub odśwież po przyjętej mutacjiKażdy ważny wiersz powinien być zabezpieczony na najtańszej wiarygodnej warstwie testów.
Testuj ładowanie jako stan, a nie jako screenshot
Zachowanie podczas ładowania powinno odpowiadać na pytania:
- Co nadal pozostaje dostępne?
- Co jest wyłączone?
- Czy użytkownik może przejść gdzie indziej?
- Czy układ pozostaje stabilny?
- Co dzieje się po wolnej odpowiedzi?
- Czy istnieje limit czasu lub ścieżka ponowienia?
- Czy ekran w dostępny sposób informuje o postępie?
Test komponentu może sprawdzać:
it('prevents duplicate checkout submission while pending', async () => {
const deferred = createDeferred();
const submit = vi.fn(() => deferred.promise);
render();
await userEvent.click(
screen.getByRole('button', { name: /place order/i }),
);
expect(
screen.getByRole('button', { name: /place order/i }),
).toBeDisabled();
await userEvent.click(
screen.getByRole('button', { name: /place order/i }),
);
expect(submit).toHaveBeenCalledTimes(1);
deferred.resolve({
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-123',
});
});Dostosuj składnię do test runnera używanego w projekcie.
Przykład korzysta z React Testing Library i składni zgodnej z Vitest; w projekcie używającym Jest należy zachować te same asercje zachowania, zamiast zmieniać strategię tylko ze względu na runner.
Najważniejsza asercja mówi, że wolna odpowiedź nie powoduje ponownego wysłania zamówienia.
Jawnie testuj błędy, po których można kontynuować
Sam komunikat błędu nie wystarcza, jeżeli użytkownik nie może odzyskać możliwości działania.
Przykład:
it('keeps entered delivery data after a recoverable failure', async () => {
const submit = vi
.fn()
.mockRejectedValueOnce(new NetworkUnavailable())
.mockResolvedValueOnce({
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-123',
});
render();
await userEvent.type(
screen.getByLabelText(/delivery note/i),
'Leave with reception',
);
await userEvent.click(
screen.getByRole('button', { name: /place order/i }),
);
expect(
await screen.findByRole('alert'),
).toHaveTextContent(/could not submit/i);
expect(
screen.getByLabelText(/delivery note/i),
).toHaveValue('Leave with reception');
await userEvent.click(
screen.getByRole('button', { name: /try again/i }),
);
expect(submit).toHaveBeenCalledTimes(2);
});Ten test chroni wysiłek użytkownika, a nie tylko komunikat błędu.
Odróżniaj odrzucenia biznesowe od awarii technicznych
Poniższe wyniki nie są równoważne:
Dostawca płatności jest niedostępny
Koszyk zawiera niedostępny produkt
Użytkownik nie ma uprawnienia
Backend zwrócił nieprawidłowe daneStosuj jawne typy wyników i kategorie błędów.
Odrzucenie biznesowe można wyrenderować jako normalny wynik procesu:
if (result.status === 'rejected') {
return showCheckoutCorrection(result.reason);
}Awaria techniczna może wymagać:
- ponowienia,
- logowania,
- metryk incydentu,
- zachowania awaryjnego.
Nie zamieniaj każdego błędu w ogólne Something went wrong.
Ogólne komunikaty mogą być właściwe ze względów bezpieczeństwa, ale wewnętrzna klasyfikacja powinna pozostać precyzyjna.
Waliduj odpowiedzi backendu na granicy
Typy TypeScript nie walidują JSON-u w czasie wykonania.
Ten kod jest niebezpieczny:
const result = await response.json() as CheckoutResult;Rzutowanie jedynie instruuje kompilator.
Nie sprawdza odpowiedzi.
Użyj przyjętej w projekcie walidacji w czasie wykonania:
const payload: unknown = await response.json();
return checkoutResultSchema.parse(payload);albo równoważnego walidatora należącego do projektu.
Testuj:
- brakujące właściwości,
- nieprawidłowe wartości enumów,
- nieprawidłowe liczby,
- nieoczekiwane wartości null,
- nieobsługiwane dodatkowe dane, gdy ma to znaczenie,
- niezgodne wersje backendu.
Naruszenie kontraktu powinno zostać wykryte blisko adaptera, zamiast później powodować niezwiązany z nim błąd komponentu.
Testuj awarie na poziomie trasy
Trasa może zawieść, zanim zostanie wyrenderowany interaktywny komponent.
Przykłady:
- błąd odczytu uwierzytelnienia,
- niedostępna usługa backendowa,
- nieprawidłowy parametr trasy,
- brak rekordu,
- odrzucenie autoryzacji,
- przekroczenie czasu w data loaderze,
- nieoczekiwany schemat odpowiedzi.
Testuj zachowanie trasy zgodnie z podejściem przyjętym w projekcie.
Sprawdź zamierzony rezultat:
Brak rekordu
→ widok not found
Nieuprawniony użytkownik
→ logowanie albo odmowa dostępu
Tymczasowa awaria backendu
→ błąd trasy z możliwością ponowienia
Nieprawidłowy parametr trasy
→ kontrolowana odpowiedź, nie nieobsłużony wyjątekKorzystaj z obecnych w projekcie konwencji frameworka dotyczących błędów, ładowania i braku zasobu.
Nie twórz własnej infrastruktury błędów, jeśli aplikacja ma już ustalony wzorzec.
Error boundary powinien umożliwiać odzyskanie działania
Error boundary nie powinien wyświetlać wyłącznie:
Something went wrong.Powinien odpowiadać na pytania:
- Co zawiodło?
- Czy awaria jest tymczasowa?
- Czy operację można ponowić?
- Czy nawigacja nadal działa?
- Czy dane wprowadzone przez użytkownika zostały zachowane?
- Czy ponowienie może zduplikować operację?
- Czy błąd został bezpiecznie zgłoszony?
Użyteczny test poziomu trasy weryfikuje:
Pierwsze żądanie zgłasza wyjątek
→ pojawia się interfejs błędu
→ użytkownik wybiera ponowienie
→ drugie żądanie kończy się powodzeniem
→ pojawia się oczekiwana treść stronyNie sprawdzaj wewnętrznych funkcji resetujących frameworka, jeśli to samo zachowanie można zweryfikować przez interfejs.
Uwierzytelnianie trzeba testować w trakcie procesu
Testowanie wyłącznie początkowego przekierowania z chronionej strony nie wystarcza.
Sesja może wygasnąć, gdy użytkownik:
- edytuje długi formularz,
- czeka na płatność,
- wysyła plik,
- przechodzi przez kilka kroków onboardingu,
- ma aplikację otwartą w innej karcie.
Realistyczny test powinien obejmować:
Użytkownik otwiera uwierzytelniony checkout
→ wprowadza dane
→ sesja wygasa
→ wysłanie zwraca unauthorized
→ użytkownik zostaje poproszony o uwierzytelnienie
→ aplikacja zachowuje bezpieczny adres powrotu
→ proces może być kontynuowany bez podwójnego wysłaniaNie zapisuj sekretów ani niebezpiecznych treści żądania w adresach URL przekierowań.
Właściwa strategia odzyskiwania zależy od wrażliwości danych.
Testy autoryzacji należą do granicy serwerowej
Ukrycie przycisku nie jest autoryzacją.
Testuj, czy nieuprawnione żądania są odrzucane także wtedy, gdy zostaną przygotowane ręcznie.
Na przykład:
Użytkownik z tenantu A żąda zamówienia z tenantu B
→ serwer odrzuca dostęp
→ odpowiedź nie zawiera chronionych danych
→ zdarzenie jest logowane zgodnie z polityką projektuTest przeglądarkowy może sprawdzać zachowanie widoczne dla użytkownika.
Test integracyjny niższego poziomu powinien weryfikować faktyczną granicę autoryzacji.
Testuj nawigację i przekierowania
Błędy przekierowań pojawiają się często, gdy:
- giną prefiksy językowe,
- znikają parametry query string,
- callback URL jest błędnie kodowany,
- przekierowanie uwierzytelniające wpada w pętlę,
- udana mutacja przekierowuje do nieaktualnych danych,
- historia przeglądarki zachowuje się nieoczekiwanie.
Dla krytycznych procesów sprawdzaj:
- docelowy URL,
- zachowanie języka,
- zachowanie bezpiecznego adresu powrotu,
- działanie przycisku Wstecz,
- bezpośrednie wejście na trasę potwierdzenia,
- odświeżenie po sukcesie.
Nie testuj każdego linku w przeglądarce.
Skup testy przeglądarkowe na przejściach procesu.
Zachowanie cache wymaga osobnych testów
Cache może sprawić, że poprawna mutacja wygląda jak zepsuta.
Przykład:
Użytkownik aktualizuje adres dostawy
→ backend zapisuje zmianę
→ aplikacja przekierowuje na stronę adresu
→ strona z cache pokazuje poprzedni adresTest powinien weryfikować kontrakt widoczny z zewnątrz:
Po zaakceptowanej mutacji kolejny istotny odczyt pokazuje nową wartość.W zależności od architektury projektu może to wymagać:
- unieważnienia cache,
- unieważnienia tagów,
- rewalidacji ścieżki lub trasy,
- wersjonowania klucza cache,
- pominięcia cache dla danych użytkownika.
Używaj mechanizmu cache faktycznie skonfigurowanego w projekcie.
Nie testuj szczegółów wewnętrznych frameworka.
Testuj, czy nieaktualne dane nie są pokazywane tam, gdzie wymagana jest świeżość.
Unikaj globalnych założeń dotyczących cache
Te same dane mogą mieć różne wymagania dotyczące świeżości:
Publiczny opis produktu
→ może tolerować krótką nieaktualność
Dostępny stan magazynowy podczas checkoutu
→ wymaga aktualnej walidacji na serwerze
Preferencja profilu użytkownika
→ zwykle powinna natychmiast odzwierciedlać zaakceptowaną zmianę
Końcowy status płatności
→ musi zostać potwierdzony przez autorytatywny backendTesty powinny odzwierciedlać wymagania biznesowe, a nie uniwersalną zasadę „cache jest dobry” albo „cache jest zły”.
Testuj aktualizacje optymistyczne także przy odrzuceniu
Interfejsy optymistyczne poprawiają odczuwaną szybkość, ale wprowadzają dodatkowe stany.
Przykład:
Użytkownik zmienia ilość produktu z 1 na 3
→ UI natychmiast pokazuje 3
→ serwer informuje, że dostępne są tylko 2 sztukiOczekiwane zachowanie musi zostać zdefiniowane:
Przywróć 1
albo
pokaż 2 jako zaakceptowane maksimum
albo
poproś użytkownika o potwierdzenie zmienionego koszykaTestuj:
- stan optymistyczny,
- zaakceptowaną odpowiedź,
- odrzuconą odpowiedź,
- rollback,
- ogłoszenie błędu,
- interakcję podczas oczekiwania.
Nie pozostawiaj zachowania rollbacku bez jawnej definicji.
Testy przeglądarkowe powinny obejmować procesy, a nie szczegóły implementacji
Test przeglądarkowy powinien opisywać cel użytkownika. Poniższy przykład używa składni Playwright, ale odpowiedzialność testu nie zależy od jednego narzędzia:
test('customer can complete checkout after correcting changed stock', async ({
page,
}) => {
await page.goto('/en/products/example-product');
await page.getByRole('button', {
name: /add to basket/i,
}).click();
await page.getByRole('link', {
name: /checkout/i,
}).click();
await page.getByLabel(/delivery address/i)
.selectOption('address-1');
await page.getByRole('button', {
name: /place order/i,
}).click();
await expect(
page.getByRole('alert'),
).toContainText(/quantity has changed/i);
await page.getByRole('button', {
name: /accept changes/i,
}).click();
await expect(page).toHaveURL(/\/orders\/[^/]+\/confirmation/);
await expect(
page.getByRole('heading', {
name: /order confirmed/i,
}),
).toBeVisible();
});Dostosuj trasy, selektory i składnię do rzeczywistej aplikacji.
Preferuj selektory dostępności zamiast szczegółów implementacyjnych CSS.
Kontroluj stan backendu w sposób celowy
Testy przeglądarkowe stają się niestabilne, gdy zależą od niekontrolowanych współdzielonych danych.
Używaj przyjętego podejścia do danych testowych:
- konfiguracji testu przez API,
- deterministycznych fixture’ów,
- izolowanej testowej bazy danych,
- dedykowanych fabryk,
- zatwierdzonej strategii resetowania bazy.
Test powinien znać:
- stan magazynowy produktu,
- uprawnienia użytkownika,
- stan koszyka,
- tryb awarii backendu,
- oczekiwany wynik zamówienia.
Nie polegaj na kontach przypominających produkcyjne, współdzielonych przez kilka testów.
Nie uruchamiaj destrukcyjnego przygotowania danych na rzeczywistym środowisku.
Symuluj awarie na kontrolowanych granicach
Przydatne punkty wstrzykiwania awarii obejmują:
Adapter API
Dostawca uwierzytelniania
Gateway płatności
Data loader
Usługa wysyłania plików
Dostawca feature flag
Endpoint statusu kolejkiUnikaj szerokiego przechwytywania, które przypadkowo mockuje niezwiązane żądania.
Test powinien jasno pokazywać zamierzoną awarię:
await checkoutApi.respondOnce({
status: 409,
body: {
code: 'basket_changed',
},
});Następnie sprawdź odpowiadające jej zachowanie naprawcze.
Nie mockuj całej aplikacji
Nadmierne mockowanie tworzy testy, które jedynie potwierdzają, że mocki zgadzają się z implementacją.
Test komponentu może zastąpić jego gateway.
Test integracyjny trasy powinien używać bardziej rzeczywistego routingu i serializacji.
Test procesu w przeglądarce powinien uruchamiać prawdziwy frontend oraz kontrolowane środowisko backendowe.
Wybierz najmniejszą granicę, która nadal dowodzi, że określone ryzyko jest zabezpieczone.
Stosuj testy kontraktowe między Next.js a backendem
Gdy Next.js korzysta z API Symfony, Laravel albo innego backendu, obie strony muszą uzgadniać:
- status HTTP,
- uwierzytelnianie,
- kształt żądania,
- kształt odpowiedzi,
- kody błędów,
- wartości nullable,
- formaty dat,
- paginację,
- idempotencję,
- wersjonowanie.
Testowanie kontraktów może opierać się na:
- schemacie API,
- współdzielonych fixture’ach,
- zapisanych i zatwierdzonych przykładach,
- testach dostawcy i konsumenta,
- bezpośrednich testach integracyjnych.
Użyteczny test sprawdza więcej niż odpowiedź 200:
it('maps a completed checkout response', async () => {
const fetcher = createFetchStub({
status: 201,
json: {
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-123',
},
});
const gateway = new HttpCheckoutGateway(
'https://backend.test',
fetcher,
);
await expect(
gateway.submit({
basketId: 'basket-1',
deliveryAddressId: 'address-1',
idempotencyKey: 'checkout-attempt-1',
}),
).resolves.toEqual({
status: 'completed',
orderId: 'order-123',
confirmationNumber: 'ORD-123',
});
});Testuj także rzeczywiste formaty błędów dostawcy.
Podwójne wysłanie wymaga idempotencji
Wyłączenie przycisku ogranicza przypadkowe duplikaty, ale nie zapewnia pełnej ochrony.
Duplikaty mogą nadal powstać przez:
- ponowienia przeglądarki,
- proxy sieciowe,
- odświeżenie strony przez użytkownika,
- wiele kart,
- ponowne dostarczenie wiadomości z kolejki,
- opóźnione odpowiedzi.
Dla mutacji mających konsekwencje backend powinien obsługiwać klucz idempotencji albo równoważne ograniczenie biznesowe.
Test frontendu powinien sprawdzać:
Jedna próba użytkownika
→ jeden stabilny klucz idempotencji
→ ponowienia wykorzystują ten sam klucz
→ nowa, zamierzona próba otrzymuje nowy kluczZa egzekwowanie idempotencji nadal odpowiada backend.
Celowo testuj warunki wyścigu
Typowy wyścig występuje, gdy użytkownik zmienia wybór, a wcześniejsze żądanie nadal oczekuje.
Przykład:
Użytkownik wyszukuje „symfony”
→ rozpoczyna się żądanie A
→ użytkownik zmienia zapytanie na „nextjs”
→ rozpoczyna się żądanie B
→ żądanie B kończy się
→ żądanie A kończy się późniejStrona nie może zastąpić nowszego wyniku nieaktualnymi danymi.
Testuj odpowiedzi kończące się poza kolejnością:
it('does not display an older search response', async () => {
const first = createDeferred();
const second = createDeferred();
const search = vi
.fn()
.mockReturnValueOnce(first.promise)
.mockReturnValueOnce(second.promise);
render();
await userEvent.type(
screen.getByRole('searchbox'),
'symfony',
);
await userEvent.clear(
screen.getByRole('searchbox'),
);
await userEvent.type(
screen.getByRole('searchbox'),
'nextjs',
);
second.resolve(resultFor('nextjs'));
expect(
await screen.findByText('Next.js result'),
).toBeVisible();
first.resolve(resultFor('symfony'));
expect(
screen.queryByText('Symfony result'),
).not.toBeInTheDocument();
});Użyj anulowania, identyfikatora żądania albo mechanizmu zarządzania stanem odpowiedniego dla implementacji.
Hydration i różnice środowiskowe wymagają uwagi
Kod może działać podczas nawigacji po stronie klienta, a zawodzić po bezpośrednim otwarciu strony.
Typowe przyczyny obejmują:
- dostęp do
windowpodczas wykonania na serwerze, - inne użycie bieżącego czasu po stronie serwera i klienta,
- różnice formatowania zależne od locale,
- losowe wartości podczas renderowania,
- dostęp do storage przeglądarki przed zamontowaniem komponentu,
- różne wartości feature flag na serwerze i kliencie.
Chroń ważne strony testami obejmującymi:
Bezpośrednią nawigację
Nawigację po stronie klienta
Odświeżenie
Wstecz i Dalej
Inny język
Stan uwierzytelniony i anonimowyNie wyciszaj ostrzeżeń hydration bez znalezienia źródła rozbieżności.
Testuj puste, częściowe i nieoczekiwane dane
Dane produkcyjne rzadko są tak kompletne, jak sugerują fixture’y.
Testuj:
- puste listy,
- brakujące opcjonalne obrazy,
- usunięte rekordy powiązane,
- nullable pola profilu,
- nieobsługiwany język,
- częściowe uprawnienia,
- nieznane wartości enumów,
- starsze rekordy backendu,
- bardzo długi tekst,
- wartości zerowe i maksymalne.
Strona nie powinna się wywracać dlatego, że brakuje jednej opcjonalnej relacji.
Jednocześnie wymagane dane biznesowe nie powinny po cichu stawać się opcjonalne tylko po to, żeby test przeszedł.
Dostępność należy do testowania procesu
Proces jest niekompletny, jeśli działa wyłącznie za pomocą myszy.
Testy komponentów i przeglądarki powinny weryfikować istotne zachowanie dotyczące:
- dostępnych nazw,
- etykiet formularzy,
- przenoszenia fokusu,
- aktywacji klawiaturą,
- komunikatów walidacyjnych,
- podsumowań błędów,
- fokusu w modalu,
- komunikatów o ładowaniu.
Po nieudanym wysłaniu formularza sprawdź, czy użytkownik potrafi odnaleźć błąd i kontynuować.
Nie polegaj wyłącznie na wizualnych screenshotach.
Testy wizualne są użyteczne, ale mają ograniczenia
Testy regresji wizualnej mogą chronić:
- duże zmiany układu,
- brakujące sekcje responsywne,
- uszkodzone szkielety ładowania,
- overflow,
- nieprawidłowe motywy.
Nie dowodzą:
- autoryzacji,
- poprawności danych API,
- idempotencji,
- udanego wysłania,
- dostępnych nazw,
- zachowania przy ponowieniu.
Używaj testów wizualnych jako jednej warstwy, a nie zamiennika asercji zachowania.
Testuj responsywne zachowanie procesu
Checkout działający na desktopie może zawieść na urządzeniu mobilnym, ponieważ:
- przycisk wysyłania jest zasłonięty przez element fixed,
- klawiatura zakrywa akcje formularza,
- podsumowanie błędów znajduje się poza viewportem,
- dialogu nie można przewijać,
- wybór adresu wymaga hover,
- nawigacja zamyka się nieoczekiwanie.
Dla krytycznych procesów stosuj niewielką liczbę testów na reprezentatywnych viewportach.
Nie duplikuj każdego testu przeglądarkowego dla wielu wymiarów.
Dobieraj kombinacje według ryzyka.
Utrzymuj testy przeglądarkowe skupione i niezależne
Test przeglądarkowy nie powinien zależeć od danych utworzonych przez poprzedni test przeglądarkowy.
Preferuj:
Przygotuj kontrolowany stan
→ wykonaj jeden proces
→ sprawdź jego wynik
→ posprzątaj przez ustaloną infrastrukturę testowąUnikaj:
Test 1 tworzy konto
→ Test 2 oczekuje tego konta
→ Test 3 modyfikuje to samo kontoWspółdzielone sekwencyjne procesy utrudniają diagnozowanie awarii.
Chroń zestaw testów przed fałszywą pewnością
Test może przechodzić z niewłaściwego powodu.
Przykłady:
- wzorzec przechwytywania żądania nigdy nie został dopasowany,
- asercja sprawdziła nieaktualny element,
- test nieoczekiwanie użył uwierzytelnienia z cache,
- formularz nie został wysłany,
- test przeglądarkowy został pominięty przez konfigurację środowiska,
- szeroki handler wyjątków ukrył błąd,
- test akceptował każdy status mniejszy niż 500.
Stosuj asercje dowodzące, że ważna akcja rzeczywiście się wydarzyła.
Na przykład:
Sprawdź payload żądania
Sprawdź wynikową trasę
Sprawdź zapisany stan backendu
Sprawdź widoczne potwierdzenieNie każdy test potrzebuje wszystkich czterech sygnałów, ale procesy krytyczne powinny zawierać więcej niż jeden.
Zachowanie produkcyjne domyka strategię testowania
Testy przed wydaniem nie odtworzą każdej kombinacji:
- przeglądarki,
- sieci,
- wolumenu danych,
- awarii dostawcy,
- momentu wdrożenia,
- stanu cache,
- długości sesji.
Weryfikacja produkcyjna powinna obejmować:
- współczynnik błędów tras,
- nieudane wywołania API,
- współczynnik ukończenia procesów,
- miejsca porzucenia checkoutu,
- pętle przekierowań uwierzytelniających,
- wyjątki po stronie klienta,
- naruszenia kontraktu backendu,
- percentyle opóźnień,
- skuteczność cache,
- wykrywanie zduplikowanych akcji.
Użyteczną metryką nie jest wyłącznie:
Checkout API zwróciło 201.Jest nią:
Użytkownik otworzył checkout
→ wysłał zamówienie
→ dotarł do stanu potwierdzonego zamówieniaMierz ukończenie procesu biznesowego tam, gdzie pozwala na to polityka prywatności i monitoringu.
Dodawaj identyfikatory korelacji
Proces może przechodzić przez:
Przeglądarkę
→ serwer Next.js
→ API backendu
→ dostawcę płatności
→ kolejkę wiadomościBez identyfikatora korelacji jedna awaria wygląda jak kilka niezwiązanych logów.
Używaj przyjętego identyfikatora żądania lub procesu, który można bezpiecznie propagować.
Nie ujawniaj bez potrzeby wrażliwych identyfikatorów wewnętrznych.
Testy mogą sprawdzać, czy identyfikator:
- jest tworzony albo przyjmowany na właściwej granicy,
- jest wysyłany do backendu,
- trafia do bezpiecznych logów,
- jest zwracany tam, gdzie to właściwe,
- nie jest nieprzewidywalnie generowany ponownie w ramach jednego procesu.
Stosuj macierz testów opartą na ryzyku
Nie każda strona wymaga takiej samej głębokości.
Klasyfikuj procesy:
Krytyczne
→ płatności, uwierzytelnianie, uprawnienia, nieodwracalne zmiany
Ważne
→ publikowanie treści, ustawienia konta, przesyłanie dokumentów
Wspierające
→ filtrowanie, opcjonalne preferencje, strony informacyjneProces krytyczny może wymagać:
- testów jednostkowych,
- testów błędów komponentów,
- testów kontraktowych adaptera,
- testów autoryzacji,
- testu udanej ścieżki w przeglądarce,
- testu odzyskiwania w przeglądarce,
- monitoringu produkcyjnego.
Funkcja wspierająca może potrzebować jedynie skupionych testów komponentowych i integracyjnych.
Celem nie jest identyczne pokrycie.
Celem jest ochrona proporcjonalna do ryzyka.
Praktyczna kolejność wdrożenia
Faza 1: Zmapuj proces
Udokumentuj:
- punkt początkowy,
- cel użytkownika,
- zależności serwerowe,
- interakcje po stronie klienta,
- końcowy autorytatywny stan.
Faza 2: Utwórz macierz awarii
Wypisz:
- błędy walidacji,
- błędy autoryzacji,
- awarie zależności,
- stany przekroczenia czasu,
- zduplikowane akcje,
- nieaktualne dane,
- częściowy sukces.
Dla każdego przypadku zdefiniuj oczekiwaną możliwość odzyskania działania.
Faza 3: Potwierdź granice architektoniczne
Zidentyfikuj:
- kontrakty warstwy aplikacyjnej,
- adaptery infrastruktury,
- moduły server-only,
- komponenty klienckie,
- walidację odpowiedzi w czasie wykonania.
Dodaj egzekwowalne reguły importów tam, gdzie projekt już je obsługuje.
Faza 4: Dodaj testy niższego poziomu
Testuj:
- reguły domenowe,
- typy wyników przypadków użycia,
- mapowanie odpowiedzi,
- klasyfikację błędów,
- zachowanie klucza idempotencji.
Faza 5: Dodaj testy zachowania komponentów
Testuj:
- walidację,
- stan oczekiwania,
- zapobieganie duplikatom,
- błędy możliwe do obsłużenia,
- zachowanie danych użytkownika,
- rollback aktualizacji optymistycznej.
Faza 6: Dodaj testy integracyjne
Testuj:
- uwierzytelnianie,
- autoryzację,
- odpowiedzi tras,
- kontrakty API,
- unieważnianie cache,
- przekierowania.
Faza 7: Dodaj krytyczne procesy przeglądarkowe
Uwzględnij:
- jedną udaną ścieżkę,
- jedną ważną ścieżkę odzyskiwania,
- jeden przypadek autoryzacji albo wygaśnięcia sesji, jeśli uzasadnia go ryzyko.
Faza 8: Dodaj sygnały produkcyjne
Mierz:
- ukończenie procesu,
- współczynnik awarii technicznych,
- skuteczność odzyskiwania,
- opóźnienie,
- naruszenia kontraktów,
- zduplikowane akcje.
Na czym powinien skupiać się code review
Podczas przeglądu zmiany w Next.js zapytaj:
- Czy dane server-only są importowane przez kod kliencki?
- Czy komponent bezpośrednio wywołuje infrastrukturę?
- Czy dane API są walidowane w czasie wykonania?
- Czy kontrole autoryzacji są egzekwowane na serwerze?
- Co dzieje się po przekroczeniu czasu zależności?
- Czy użytkownik może bezpiecznie ponowić operację?
- Czy mutację można wysłać dwukrotnie?
- Czy klucz idempotencji pozostaje stabilny między ponowieniami?
- Czy zaakceptowane dane są unieważniane albo odświeżane?
- Czy nieaktualne żądanie może nadpisać nowszy stan?
- Czy bezpośrednia nawigacja zachowuje się tak samo jak nawigacja kliencka?
- Czy stany ładowania, pusty i błędu są dostępne?
- Czy test chroni zachowanie, czy szczegóły implementacji?
- Czy pełny ważny proces jest gdzieś zabezpieczony?
- Czy monitoring produkcyjny ujawni awarię procesu?
Reguły rozwoju wspieranego przez AI
Asystentowi programistycznemu AI nie należy zlecać wyłącznie:
Dodaj testy do tej strony.Przekaż mu jawne odpowiedzialności:
Sprawdź stronę, kontrakt warstwy aplikacyjnej, adapter API i istniejące testy.
Zidentyfikuj:
- stany sukcesu,
- stany ładowania,
- odrzucenia biznesowe,
- awarie techniczne,
- zmiany uwierzytelnienia,
- ryzyko podwójnego wysłania,
- unieważnianie cache,
- kompletny proces użytkownika.
Użyj istniejącego test runnera i frameworka testów przeglądarkowych.
Nie dodawaj pakietów.
Nie zastępuj interfejsów należących do projektu bezpośrednimi wywołaniami fetch.
Nie sprawdzaj prywatnych szczegółów implementacji.
Dla każdego ryzyka dodaj najmniejszą warstwę testu, która go dowodzi.Przed zaakceptowaniem testów wygenerowanych przez AI sprawdź, czy mogą zawieść przy zamierzonym błędzie.
Test, który nie może zawieść, gdy zachowanie jest zepsute, jest dokumentacją, a nie ochroną.
Wnioski senior developera
Testowanie aplikacji Next.js nie jest konkursem na największą liczbę testów.
Celem jest stworzenie dowodów, że architektura pozostaje zrozumiała, awarie są kontrolowane, a użytkownicy mogą wykonać ważną pracę.
Dojrzała strategia zwykle chroni:
- kierunek zależności,
- odpowiedzialności serwera i klienta,
- kontrakty API w czasie wykonania,
- uwierzytelnianie i autoryzację,
- ładowanie i odzyskiwanie po błędach,
- idempotentne mutacje,
- cache i świeżość danych,
- warunki wyścigu,
- krytyczne procesy przeglądarkowe,
- ukończenie procesów na produkcji.
Najsilniejszy zestaw testów nie próbuje wykonywać każdej możliwej kombinacji przez przeglądarkę.
Umieszcza każde ryzyko na najniższej wiarygodnej warstwie i łączy te warstwy niewielką liczbą realistycznych procesów użytkownika.
To właśnie sprawia, że aplikację można bezpiecznie refaktoryzować.
Zwiększa to również bezpieczeństwo rozwoju wspieranego przez AI. Narzędzie AI może szybko generować komponenty, adaptery i testy, ale projekt musi wcześniej definiować granice, oczekiwania wobec awarii oraz procesy, które wygenerowany kod ma zachować.
Udane żądanie nie jest tym samym co udany proces użytkownika.
Wyrenderowana strona nie jest tym samym co chroniona architektura.
Przechodzący zestaw testów ma wartość tylko wtedy, gdy zawodzi przy problemach, przed którymi biznes rzeczywiście musi się chronić.
