Cypress Component Testing w React i Next.js — kiedy naprawdę ma sens

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.

Opublikowano

6 października 2025 08:20

Czytanie

6 min czytania

Aktualizacja

7 kwietnia 2026 10:47

Cypress Component Testing (CT) daje nam przydatną warstwę testów, kiedy chcemy wyłapać problemy z DOM, CSS i interakcjami, których nie widać w samym jsdom. Testując komponet odpalamy go w przeglądarce, ale bez uruchamiania całej aplikacji.

Krótka odpowiedź: Cypress CT = prawdziwa przeglądarka oraz izolowany komponent bez uruchamiania całej aplikacji. Użyj go dla: CSS-zależnych komponentów, drag & drop, focus states. Użyj RTL dla logiki, hooków i szybkich unit testów, a E2E dla: routingu, auth, pełnych flow. Pamiętaj: CT nie zastępuje ani React Testing Library ani pełnych testów E2E. W projektach Next.js najlepiej radzi sobie z client components, design systemu, formularzy, interakcji z DOM i przypadków, w których chcesz zobaczyć prawdziwy CSS. Z kolei dla całych stron, Server Components, redirectów, autoryzacji i przepływów między widokami, dalej wygrywają testy E2E.

Czym Component Testing różni się od E2E?

Różnica jest zasadnicza: w tradycyjnym Cypress E2E musisz mieć uruchomiony serwer dev, cała aplikacja jest włączona i działa, a Cypress nawiguje po stronach jak użytkownik. Z kolei w Component Testing montujesz pojedynczy komponent (lub drzewo komponentów) bezpośrednio w przeglądarce Cypress.

Code
E2E:   Cypress → przeglądarka → Twoja aplikacja (next dev) → komponent
CT:    Cypress → przeglądarka → komponent (bezpośrednio)

W ten sposób, testy CT są szybsze, bardziej izolowane i łatwiejsze do debugowania.

Setup: Next.js + TypeScript + Cypress CT

Instalacja

Code
npm install -D cypress
npx cypress open

Przy pierwszym uruchomieniu Cypress daje nam wybór pomiędzy E2E Testing, a Component Testing - wybierz Component Testing i pozwól mu wygenerować bazową konfigurację. W przypadku Next.js Component Testing, opiera się zwykle na integracji z Webpackiem, więc warto potraktować to jako osobną warstwę testów dla komponentów, a nie próbę uruchamiania całej aplikacji App Router 1:1 wewnątrz Cypressa.

Konfiguracja

Code
// cypress.config.ts
import { defineConfig } from 'cypress'
 
export default defineConfig({
  component: {
    devServer: {
      framework: 'next',
      bundler: 'webpack',
    },
    specPattern: 'cypress/component/**/*.cy.{ts,tsx}',
    supportFile: 'cypress/support/component.ts',
  },
})

Support file

Code
// cypress/support/component.ts
import './commands'
import { mount } from 'cypress/react' // Cypress 13+; w Cypress 10–12 użyj 'cypress/react18'
 
declare global {
  namespace Cypress {
    interface Chainable {
      mount: typeof mount
    }
  }
}
 
Cypress.Commands.add('mount', mount)

Pierwszy Component Test

Zacznijmy od prostego komponentu Alert:

Code
// components/Alert.tsx
interface AlertProps {
  type: "success" | "error" | "warning";
  message: string;
  onClose?: () => void;
}
 
export function Alert({ type, message, onClose }: AlertProps) {
  return (
    <div role="alert" className={`alert alert-${type}`}>
      <span>{message}</span>
      {onClose && (
        <button onClick={onClose} aria-label="Zamknij">
          ×
        </button>
      )}
    </div>
  );
}
Code
// cypress/component/Alert.cy.tsx
import { Alert } from "@/components/Alert";
 
describe("Alert", () => {
  it("renders success message", () => {
    cy.mount(<Alert type="success" message="Zapisano pomyślnie!" />);
 
    cy.get("[role='alert']")
      .should("be.visible")
      .and("have.class", "alert-success");
    cy.contains("Zapisano pomyślnie!");
  });
 
  it("renders error variant", () => {
    cy.mount(<Alert type="error" message="Coś poszło nie tak" />);
 
    cy.get("[role='alert']").should("have.class", "alert-error");
  });
 
  it("calls onClose when dismiss button is clicked", () => {
    const onClose = cy.stub().as("onCloseHandler");
 
    cy.mount(
      <Alert type="warning" message="Uwaga!" onClose={onClose} />
    );
 
    cy.get("[aria-label='Zamknij']").click();
    cy.get("@onCloseHandler").should("have.been.calledOnce");
  });
 
  it("does not render close button without onClose prop", () => {
    cy.mount(<Alert type="success" message="Info" />);
 
    cy.get("[aria-label='Zamknij']").should("not.exist");
  });
});

Uruchom testy:

Code
npx cypress open --component

Testowanie komponentów z hookami i stanem

Code
// components/SearchInput.tsx
import { useState, useEffect } from "react";
import { useDebounce } from "@/hooks/useDebounce";
 
interface SearchInputProps {
  onSearch: (query: string) => void;
  placeholder?: string;
  debounceMs?: number;
}
 
export function SearchInput({
  onSearch,
  placeholder = "Szukaj...",
  debounceMs = 300,
}: SearchInputProps) {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, debounceMs);
 
  useEffect(() => {
    if (debouncedQuery) {
      onSearch(debouncedQuery);
    }
  }, [debouncedQuery, onSearch]);
 
  return (
    <input
      type="search"
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder={placeholder}
      aria-label="Pole wyszukiwania"
    />
  );
}
Code
// cypress/component/SearchInput.cy.tsx
import { SearchInput } from "@/components/SearchInput";
 
describe("SearchInput", () => {
  it("calls onSearch after debounce", () => {
    cy.clock(); // przejmuje kontrolę nad timerami przeglądarki
    const onSearch = cy.stub().as("searchHandler");
 
    cy.mount(<SearchInput onSearch={onSearch} debounceMs={200} />);
 
    cy.get("[aria-label='Pole wyszukiwania']").type("React");
 
    // Przed debounce – onSearch nie powinien być wywołany
    cy.get("@searchHandler").should("not.have.been.called");
 
    // Przesuń czas o 200 ms – debounce odpala się dokładnie teraz
    cy.tick(200);
    cy.get("@searchHandler").should("have.been.calledWith", "React");
  });
 
  it("renders custom placeholder", () => {
    cy.mount(
      <SearchInput onSearch={cy.stub()} placeholder="Znajdź produkt..." />
    );
 
    cy.get("input").should("have.attr", "placeholder", "Znajdź produkt...");
  });
});

Testowanie z kontekstem i providerami

Wiele komponentów React zależy od kontekstu (Theme, Auth, Router), natomiast w CT musisz dostarczyć te providery ręcznie.

Code
// cypress/support/component.ts – rozszerzony
import { mount } from "cypress/react";
import { ThemeProvider } from "@/context/ThemeContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 
function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
    },
  });
 
  return function Wrapper({ children }: { children: React.ReactNode }) {
    return (
      <QueryClientProvider client={queryClient}>
        <ThemeProvider>{children}</ThemeProvider>
      </QueryClientProvider>
    );
  };
}
 
Cypress.Commands.add("mount", (component, options = {}) => {
  const Wrapper = createWrapper();
  return mount(<Wrapper>{component}</Wrapper>, options);
});

Teraz każdy cy.mount() automatycznie opakowuje komponent w potrzebne providery.

Cypress CT vs Jest + React Testing Library

AspektCypress CTJest + RTL
ŚrodowiskoPrawdziwa przeglądarkajsdom (symulacja)
CSSDziała (widoczne wizualnie)Ignorowane
Visual debuggingTime-travel, snapshotyConsole.log
SzybkośćWolniejsze (przeglądarka)Szybsze (Node.js)
Event handlingNatywne eventy przeglądarkiSymulowane
SetupWymaga konfiguracji webpackProsta konfiguracja
CIWymaga headless browserNie wymaga

Kiedy Cypress CT wygrywa?

W telegraficznym skrócie:

  • Komponent intensywnie korzysta z CSS (animacje, responsive layout),
  • Zależy Ci na wizualnym debugowaniu,
  • Testujesz interakcje drag & drop, scroll, resize,
  • Chcesz spójności z istniejącymi testami Cypress E2E.

Kiedy Jest + RTL wygrywa?

  • Kiedy szybkość jest priorytetem (CI/CD to praktyka automatyzacji testów, buildów i wdrożeń w procesie dostarczania oprogramowania.),
  • Testujesz logikę, a nie wygląd i masz prosty setup bez ciężkich zależności CSS,
  • Twój zespół już zna i używa z powodzeniem RTL.

Interceptowanie requestów w CT

Cypress CT wspiera cy.intercept() tak samo jak E2E:

Code
// cypress/component/UserProfile.cy.tsx
import { UserProfile } from "@/components/UserProfile";
 
describe("UserProfile", () => {
  it("displays user data from API", () => {
    cy.intercept("GET", "/api/user/1", {
      statusCode: 200,
      body: { name: "Jan Kowalski", email: "jan@example.com" },
    }).as("getUser");
 
    cy.mount(<UserProfile userId="1" />);
 
    cy.wait("@getUser");
    cy.contains("Jan Kowalski");
    cy.contains("jan@example.com");
  });
 
  it("shows error state on API failure", () => {
    cy.intercept("GET", "/api/user/1", {
      statusCode: 500,
      body: { error: "Internal Server Error" },
    });
 
    cy.mount(<UserProfile userId="1" />);
 
    cy.contains(/nie udało się załadować/i);
  });
});

Ograniczenia w projektach Next.js

To jest miejsce, w którym wiele osób próbuje wycisnąć z CT za dużo.

  • Client Components nadają się bardzo dobrze do CT, poniważ posiadają lokalny stan, eventy i zależności od DOM.
  • Server Components lepiej testować przez integrację lub E2E, ponieważ ich wartość wynika z renderowania po stronie serwera, cachingu i kompozycji z resztą aplikacji.
  • Routing App Routera nie jest celem CT. Jeśli chcesz testować przejścia między stronami, middleware, auth guardy czy redirect(), pisz testy E2E.
  • generateMetadata, generateStaticParams, route handlers i cache Next.js to nie są "zachowania komponentu" i nie warto na siłę wrzucać ich do CT.

Dobry podział odpowiedzialności wygląda zwykle tak:

  • Vitest/RTL: czysta logika, hooki, komponenty bez ciężkiego CSS,
  • Cypress CT: komponenty interaktywne, layout zależny od przeglądarki, drag and drop, focus states,
  • Cypress/Playwright E2E: krytyczne flow użytkownika, routing, autentykacja, checkout.

Porady praktyczne

Organizacja plików – trzymaj testy CT blisko komponentów lub w dedykowanym cypress/component/:

Code
├── components/
│   ├── Alert.tsx
│   └── SearchInput.tsx
├── cypress/
│   ├── component/
│   │   ├── Alert.cy.tsx
│   │   └── SearchInput.cy.tsx
│   └── e2e/
│       └── checkout.cy.ts

Data-testid vs role – preferuj role i aria-label (jak RTL), ale data-testid to pragmatyczny fallback.

Nie testuj w CT tego, co lepiej sprawdzi E2E – nawigacja między stronami, pełne flow użytkownika i integracja wielu komponentów to domena testów E2E.

FAQ

Czym jest Cypress Component Testing?

Cypress Component Testing (CT) to tryb testowania, w którym montujesz pojedynczy komponent React bezpośrednio w przeglądarce Cypress i to bez uruchamiania całej aplikacji i serwera dev. Otrzymujesz w ten sposób prawdziwe środowisko przeglądarki, działający CSS, natywne eventy DOM, time-travel debugging. To dodatkowa warstwa między unit testami (jsdom), a pełnymi testami E2E.

Jaka różnica między Cypress CT, a Jest + React Testing Library?

Cypress CT działa w prawdziwej przeglądarce, co oznacza, że CSS jest aplikowany, eventy są natywne, a animacje działają - daje pewność, że komponent zachowuje się jak w produkcji, co jest wartościowe dla komponentów CSS-intensywnych i złożonych interakcji DOM. Jest + RTL używa jsdom, czyli symuluje przeglądarkę bez prawdziwego renderowania CSS, natomiast sam RTL jest szybszy (Node.js, nie przeglądarka) i prostszy w konfiguracji.

Kiedy wybrać Cypress CT zamiast RTL?

Cypress CT wygrywa gdy: (1) komponent intensywnie używa CSS (animacje, media queries, focus states); (2) testujesz interakcje drag & drop, scroll, resize; (3) zależy Ci na wizualnym debugowaniu i inspekcji w przeglądarce; (4) już używasz Cypress E2E i chcesz spójnego workflow. RTL wygrywa gdy: (1) szybkość w CI jest priorytetem; (2) testujesz logikę i hooki; (3) masz prosty stack bez CSS dependencies.

Czy można testować Next.js Server Components w Cypress CT?

Nie bezpośrednio. Server Components wykonują się po stronie serwera i zwracają serialized payload — nie są komponentami React, które można zamontować w przeglądarce. Do testowania Server Components używaj testów integracyjnych lub E2E. Cypress CT najlepiej sprawdza się dla Client Components z 'use client', które mają lokalny stan, handlery zdarzeń i bezpośrednie interakcje z DOM.

Jak skonfigurować Cypress Component Testing w Next.js?

Instalacja: npm install -D cypress. Przy pierwszym npx cypress open wybierz "Component Testing" i framework Next.js — Cypress wygeneruje bazową konfigurację. W cypress.config.ts ustaw component.devServer.framework: 'next' i bundler: 'webpack'. Dodaj support file z Cypress.Commands.add('mount', mount). Dla komponentów zależnych od kontekstu (React Query, Theme) stwórz wrapper w support file.

Czy Cypress CT zastępuje testy E2E?

Nie. CT i E2E testują różne rzeczy, ponieważ CT oznacza izolowany komponent, szybkie wykonanie, bez routingu ani serwera. E2E wiąże się z pełną aplikacją uruchomioną jak w produkcji, realne przepływy użytkownika przez wiele stron. Co wymaga E2E? Routing, middleware, auth, redirecty, generateMetadata czy Server Actions. Optymalny podział to RTL dla logiki, CT dla interaktywnych komponentów z realnym DOM, a E2E dla krytycznych flow.

Jak mockować API w Cypress Component Testing?

Cypress CT wspiera cy.intercept() tak samo jak E2E, czyli przechwytuje requesty HTTP i zwraca mock response. W teście: cy.intercept('GET', '/api/user', { body: mockUser }), potem montuj komponent i weryfikuj render. Możesz też używać cy.stub() do mockowania propsów będących callbackami. Dla komponentów używających React Query warto skonfigurować QueryClient z retry: false w wrapperze testowym.

Źródła i dokumentacja

Podsumowanie

Cypress Component Testing to potężne narzędzie, które wypełnia lukę między unit testami, a testami E2E. Daje Ci pewność prawdziwej przeglądarki z szybkością izolowanego testu, co w połączeniu z Next.js i TypeScript tworzy bardzo solidny fundament jakości kodu w projekcie.

Jeśli już korzystasz z Cypress do E2E, CT jest Twoim naturalnym następnym krokiem. Z kolei, jeśli zaczynasz od zera, traktuj go jako narzędzie do konkretnych problemów: realnego DOM, CSS i trudnych interakcji.

Najlepszy setup? Rozsądny podział między CT, RTL i E2E...

Pracuję z tym zawodowo.

Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.

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.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab