Niestabilne testy E2E blokują potok CI CD zamiast go zabezpieczać. Zobacz jak skonfigurować Cypress w Next.js App Router aby testy były zawsze szybkie i powtarzalne.
Maciej Sala
Founder StriveLab
4 min czytaniaOpublikowano 31 października 2025 (Aktualizacja 25 maja 2026)
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/, żeby izolować testy od stanu backendu i eliminować niedeterministyczne zachowania.
Custom commands eliminują powtarzalny kod, taki jak logowanie, nawigacja i 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ć, dlatego 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, a 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, na przykład 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, lecz przepaść, która ukrywa błędy routingu, middleware i cache do momentu wdrożenia.
cy.session() do cache'owania logowania skraca testy, bo 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 są 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, które 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, bo 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, bo 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, bo 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 dodawaj tylko if: failure(), a nagrania if: always(). Dla dużych zestawów testów rozważ paralelizację przez matrix strategy.
O autorze
Maciej Sala
Maciej Sala — konsultant technologiczny produktów cyfrowych i web 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 rozwija 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
Playwright idealnie sprawdza się w projektach opartych na Next.js App Router, ponieważ testuje aplikację dokładnie tak, jak widzi ją użytkownik: od uwzględnienia streamingu HTML, przez płynną nawigację, aż po formularze i interakcje. Wbudowane funkcje, takie jak automatyczne oczekiwanie, fixtures, zaawansowane zarządzanie żądaniami oraz testy wizualne, pozwalają kompleksowo zabezpieczyć jakość kodu bez konieczności dokładania kolejnych, zewnętrznych narzędzi.