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 – 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)})
Krok 5: Testowanie App Router – specyficzne sprawy
Loading states i Suspense
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
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 API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami. Tu chodzi o konwencje plików Next.js, które zamieniają eksportowaną funkcję w gotowy plik sitemap.xml lub robots.txt. partnera albo webhook.
Parallel Routes i Intercepting Routes
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:
// W teście:beforeEach(() => { cy.task('seedDatabase')})afterEach(() => { cy.task('cleanDatabase')})
Często zadawane pytania
O autorze
Maciej Sala
Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.
Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje 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.