Grupuj testy według feature'ów, nie typów stron. W złożonych projektach nawigacja po auth/, products/, checkout/ jest o rząd wielkości szybsza niż szukanie testu w płaskiej liście plików.
Krok 3: fixtures i dane testowe
Fixtures to statyczne pliki JSON z danymi testowymi. Trzymaj je w cypress/fixtures/ — izolują testy od stanu backendu i eliminują niedeterministyczne zachowania.
Custom commands eliminują powtarzalny kod — logowanie, nawigacja, setup stanu. Im mniej duplikatu w testach, tym niższy koszt utrzymania całego zestawu.
Code
// cypress/support/commands.ts// Logowanie przez UI (wolne, ale testuje faktyczny flow)Cypress.Commands.add('loginViaUI', (email: string, password: string) => { cy.visit('/login') cy.get("[data-testid='email-input']").type(email) cy.get("[data-testid='password-input']").type(password) cy.get("[data-testid='login-button']").click() cy.url().should('not.include', '/login')})// Logowanie przez API (szybkie, dla testów nie dotyczących auth)// Uwaga: działa wyłącznie z tokenami w localStorage (JWT). Przy auth opartym// na HttpOnly cookies (NextAuth, Auth.js) użyj loginViaUI lub cy.session().Cypress.Commands.add('loginViaAPI', (email: string, password: string) => { cy.request({ method: 'POST', url: '/api/auth/login', body: { email, password }, }).then((response) => { cy.window().then((win) => { win.localStorage.setItem('token', response.body.token) }) })})// Logowanie przez session caching (najszybsze)Cypress.Commands.add('login', (email?: string, password?: string) => { const userEmail = email ?? Cypress.env('TEST_USER_EMAIL') const userPassword = password ?? Cypress.env('TEST_USER_PASSWORD') cy.session( [userEmail], () => { cy.loginViaAPI(userEmail, userPassword) }, { validate: () => { // Sprawdź czy sesja jest nadal aktywna; bez tego Cypress może // odtwarzać wygasłą sesję bez ponownego logowania cy.request({ url: '/api/auth/me', failOnStatusCode: false }) .its('status') .should('eq', 200) }, }, )})// Helper do sprawdzania toast notificationsCypress.Commands.add('shouldShowToast', (message: string) => { cy.get("[role='status']").should('contain.text', message)})
Next.js App Router intensywnie korzysta z loading.tsx i Suspense. Testy muszą to uwzględniać — sprawdzają nie tylko dane końcowe, ale cały cykl życia komponentu:
Code
it('shows loading skeleton while fetching data', () => { cy.intercept('GET', '/api/products', (req) => { req.on('response', (res) => { res.setDelay(2000) // symuluj wolny response }) }) cy.visit('/products') // Sprawdź loading state cy.get("[data-testid='product-skeleton']").should('be.visible') // Poczekaj na dane cy.get("[data-testid='product-card']", { timeout: 5000 }).should( 'have.length.at.least', 1, ) // Loading powinien zniknąć cy.get("[data-testid='product-skeleton']").should('not.exist')})
Server Actions w testach E2E
Uzależnianie testów od wewnętrznych mechanizmów Next.js to prosta droga do
stworzenia niestabilnego zestawu, który wymaga poprawek po każdej aktualizacji
frameworka.
Testuj efekt widoczny dla użytkownika — nie mechanizm:
Code
it('submits form via Server Action', () => { cy.login() cy.visit('/settings') cy.get("[name='displayName']").clear().type('Nowa Nazwa') cy.get("[data-testid='save-button']").click() cy.shouldShowToast('Ustawienia zapisane') cy.contains('Nowa Nazwa').should('be.visible')})
Intercepcję zachowaj do zewnętrznych usług — gdy formularz wywołuje Stripe, zewnętrzne partnera albo webhook.
Parallel Routes i Intercepting Routes w testach E2E
Code
it('opens product modal via intercepting route', () => { cy.visit('/products') cy.get("[data-testid='product-card']").first().click() // Modal powinien się otworzyć (intercepting route) cy.get("[role='dialog']").should('be.visible') cy.get("[role='dialog']").contains('Laptop ThinkPad X1') // URL się zmienił cy.url().should('include', '/products/1') // Zamknij modal cy.get("[aria-label='Zamknij']").click() cy.get("[role='dialog']").should('not.exist') cy.url().should('eq', `${Cypress.config('baseUrl')}/products`)})
Testy CI zawsze uruchamiaj przeciwko next build + next start. Różnica między next dev a buildem produkcyjnym to nie szczegół techniczny — to przepaść, która ukrywa błędy routingu, middleware i cache do momentu wdrożenia.
cy.session() do cache'owania logowania — bez tego każdy test traci czas na powtórne logowanie i rośnie całkowity czas CI.
Testuj critical paths, nie każdą stronę — E2E testy są kosztowne, więc skoncentruj się na flow związanym z funkcjonowaniem biznesu: checkout, signup, core feature.
Selektory data-testid — odporne na refactor. Klasy CSS i teksty zmieniają się przy redesignie i wymagają aktualizacji dziesiątek testów.
Eliminuj cy.wait(ms) — używaj cy.wait("@alias") lub assertions z timeoutem, ponieważ hardkodowane waity to flaky testy — kosztują czas i zaufanie do zestawu.
Seeduj bazę danych przed testami — cy.task() do setupu i cleanup eliminuje wzajemne zanieczyszczanie testów:
Do lokalnego developmentu można używać next dev, ale w CI testuj zbudowaną aplikację przez next build i next start — tylko wtedy odtwarzasz warunki zbliżone do produkcji.
Ile testów E2E warto mieć?
Mniej stabilnych testów pokrywających krytyczne ścieżki jest warte więcej niż duża liczba kruchych scenariuszy zależnych od szczegółów implementacji.
Jak ograniczyć flaky testy w Cypressie?
Kluczem do sukcesu są selektory data-testid, fixtures, custom commands, kontrola stanu testowego, eliminacja arbitralnych timeoutów i uruchamianie testów przeciwko stabilnej wersji aplikacji.
Dlaczego warto pisać testy E2E w Next.js?
Testy E2E weryfikują całą ścieżkę użytkownika, tj. od kliknięcia przycisku, przez żądanie HTTP, aż po zmianę widoczną w UI. W przeciwieństwie do testów jednostkowych nie testują izolowanych funkcji, tylko faktyczne działanie aplikacji w przeglądarce. W Next.js App Router, gdzie renderowanie serwer-klient potrafi być bardziej złożone, testy E2E wychwytują błędy integracyjne, których testy jednostkowe nie obejmują.
Czym są fixtures w Cypress i kiedy ich używać?
Fixtures to statyczne pliki JSON (lub inne) z danymi testowymi, przechowywane w cypress/fixtures/. Używaj ich do mockowania odpowiedzi API (cy.intercept()), dostarczania danych wejściowych formularzy i izolowania testów od zewnętrznych zależności. Dzięki fixtures test jest przewidywalny i powtarzalny — niezależnie od stanu backendu.
Jak unikać flaky testów w Cypress?
Nie używaj cy.wait(ms) z hardcoded czasem — zamiast tego czekaj na aliasowany request (cy.wait('@alias')) lub używaj asercji z timeoutem. Stosuj selektory data-testid zamiast klas CSS lub tekstu. W CI uruchamiaj testy na zbudowanej aplikacji, nie dev serwerze. Włącz retries: { runMode: 2 } dla CI, by odfiltrować sporadyczne problemy środowiskowe.
Jak działa `cy.session()` i dlaczego przyspiesza testy?
cy.session() cache'uje stan sesji (cookies, localStorage, sessionStorage) po pierwszym logowaniu i przywraca go dla kolejnych testów zamiast logować się każdorazowo. Testy wymagające uwierzytelnionego użytkownika nie przechodzą pełnego procesu logowania — sesja jest zapisana i odtwarzana, co znacząco skraca czas wykonania całego zestawu.
Jak konfigurować zmienne środowiskowe w Cypress dla CI?
Zmienne podawaj przez env w cypress.config.ts dla wartości niepoufnych lub przez plik cypress.env.json (w .gitignore) lokalnie. W GitHub Actions używaj CYPRESS_ prefix w sekcji env workflowa, a wartości przechowuj w GitHub Secrets. Cypress automatycznie mapuje CYPRESS_FOO na Cypress.env('FOO') w testach.
Czy powinienem testować Server Actions bezpośrednio w Cypress?
Nie. Przywiązywanie testów do wewnętrznego transportu Next.js prowadzi do destabilizacji — testy łamią się przy zmianach frameworka. Testuj efekt widoczny dla użytkownika: wypełnij formularz, kliknij zapisz, sprawdź czy pojawił się toast i zaktualizowana treść. Intercepcję stosuj do zewnętrznych usług (Stripe, zewnętrzne API), nie do wewnętrznych mechanizmów Next.js.
Jak zintegrować Cypress z GitHub Actions?
Użyj oficjalnej akcji cypress-io/github-action@v6 zamiast ręcznego konfigurowania: dostarcza parametry start (komenda uruchamiająca serwer) i wait-on (URL do odczekania przed startem testów). Artefakty (screenshoty przy błędach, nagrania wideo) uploaduj przez actions/upload-artifact — screenshoty tylko if: failure(), nagrania if: always(). Dla dużych zestawów testów rozważ paralelizację przez matrix strategy.
O autorze
Maciej Sala
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.
Cypress Component Testing w React i Next.js bez marketingowej mgły. Kiedy daje przewagę nad RTL, jak go skonfigurować i gdzie kończą się jego możliwości.
Maciej Sala
Founder StriveLab
Dogłębne porównanie Cypress i Playwright w kontekście projektów Next.js. doświadczenie deweloperskie, szybkość, wsparcie dla SSR i React – który framework testowy wybrać?
Maciej Sala
Founder StriveLab
Jedna linijka w niewłaściwym miejscu — , która miała zostać tylko na środowisku testowym — i strona znika z Google na tygodnie. To jeden z najdroższych błędów w SEO technicznym, ponieważ długo pozostaje niewidoczny: build przechodzi, strona działa, użytkownicy niczego nie zauważają, a ruch organiczny po cichu się osuwa. Dobra wiadomość jest taka, że tę klasę błędów da się złapać automatycznie, zanim kod w ogóle trafi na produkcję. W tym artykule pokazuję, jak zbudować testy regresji SEO w Playwright i wpiąć je w GitHub Actions , żeby pull request z zepsutym linkiem kanonicznym czy przypadkowym noindexem po prostu nie przeszedł.