Cypress, testy end to end wewnątrz przeglądarki
Cypress to narzędzie do testów end to end, w którym kod testu wykonuje się w tej samej pętli zdarzeń co testowana aplikacja. Bieżąca wersja to 15.21.0 z 18 sierpnia 2026 roku, repozytorium cypress-io/cypress ma około 51 tysięcy gwiazdek, a pakiet npm jest na licencji MIT. Płatna jest usługa Cypress Cloud, a nie samo narzędzie.
Co Cypress właściwie robi
Różnica między Cypressem a resztą stawki sprowadza się do jednego zdania z oficjalnej dokumentacji: polecenia Cypressa działają wewnątrz przeglądarki. Nie ma protokołu przewodowego, nie ma serializacji obiektów do JSON, nie ma procesu w Node, który przez gniazdo sieciowe każe przeglądarce kliknąć w element. Plik ze specyfikacją ładuje się do tej samej karty co aplikacja, w sąsiedniej ramce, i ma bezpośredni dostęp do jej obiektu window, do modelu dokumentu i do wszystkiego, co aplikacja wystawia.
Z tego wynika cała reszta, dobra i zła. Dobra strona to interfejs. Po uruchomieniu cypress open dostajesz okno z dziennikiem poleceń po lewej i aplikacją po prawej. Każde polecenie zostawia migawkę stanu strony, więc najechanie myszą na wpis cy.get('[data-cy=submit]') przywraca podgląd aplikacji do chwili sprzed kliknięcia. Działa to po zakończeniu testu, bez ponownego uruchamiania czegokolwiek. Konkurencja odtwarza ten sam efekt z nagrania, a nie z żywego stanu, i to jest realna różnica w codziennej pracy.
Druga konsekwencja to automatyczne ponawianie. Polecenia nie zwracają wartości od razu, tylko dopisują się do kolejki i odpytują model dokumentu, aż zapytanie się powiedzie albo minie defaultCommandTimeout, czyli domyślnie 4000 milisekund. Asercje takie jak .should('be.visible') są ponawiane tak samo. W testach Cypressa prawie nie ma więc jawnego czekania na warunek, bo mechanizm robi to sam.
Trzecia to ograniczenia, które dokumentacja nazywa wprost trwałymi. Kod testu wykonuje się w przeglądarce, więc jest to zawsze JavaScript i nigdy nic innego. Nie da się sterować dwiema przeglądarkami naraz. Każdy test jest związany z jedną nadrzędną domeną. Nie ma polecenia przełączającego kontekst do ramki iframe, choć ramki z tej samej domeny odpytasz zwykłym cy.get. Do rozmowy z bazą danych albo z zapleczem służą cy.task() i cy.request(), bo modułu napisanego dla Node po prostu nie zaimportujesz do specyfikacji.
Cypress obsługuje też testy komponentów, w tym samym oknie i z tym samym dziennikiem poleceń. To bezpośrednia konkurencja dla testów w Vitest z biblioteką do renderowania w pamięci, tyle że tutaj komponent renderuje się w prawdziwej przeglądarce i widzisz go na ekranie.
Wersja, licencja i sposób dystrybucji
Wydania idą regularnie co mniej więcej dwa tygodnie. Wersje od 15.16.0 do 15.21.0 ukazały się między 26 maja a 18 sierpnia 2026 roku. Pole engines wymaga Node w wersji ^20.1.0 || ^22.0.0 || >=24.0.0, więc Node 18 i Node 21 są poza wsparciem. Pakiet ma 39 zależności bezpośrednich.
Licencja wymaga trzech sprawdzeń, bo źródła się rozjeżdżają. Plik LICENSE w repozytorium zawiera tekst MIT z notą „Copyright (c) 2023 Cypress.io". Pole license w rejestrze npm dla pakietu cypress ma wartość MIT. Natomiast opublikowana paczka, po rozpakowaniu 836 wpisów z archiwum, nie zawiera ani jednego pliku licencyjnego. Nie ma LICENSE, nie ma LICENSE.md, nie ma COPYING. Jeśli Twój proces zgodności zbiera teksty licencji z katalogu node_modules, dla Cypressa nie zbierze niczego i zgłosi zależność jako nieopisaną.
Druga część tej historii jest ciekawsza. Pakiet cypress z npm to w zasadzie sam program pobierający. W jego package.json stoi "postinstall": "node dist/index.js --exec install", a ten skrypt sięga po właściwe binarium do serwisu download.cypress.io. Archiwum z npm waży poniżej megabajta, natomiast to, co realnie uruchamiasz, przychodzi osobnym kanałem, poza rejestrem i poza jego mechanizmem weryfikacji. Zachowanie da się nagiąć zmiennymi CYPRESS_INSTALL_BINARY, CYPRESS_DOWNLOAD_MIRROR i CYPRESS_CACHE_FOLDER, i w środowisku odciętym od internetu i tak trzeba to zrobić. Praktyczny wniosek jest taki, że licencja MIT opisuje kod w repozytorium, a nie plik wykonywalny, który ląduje w pamięci podręcznej na maszynie budującej.
Osobno stoi Cypress Cloud. To zamknięta usługa komercyjna prowadzona przez tę samą firmę i nic z jej kodu nie jest publiczne. Od wersji 15.21.0 dostępna jest też zmienna CYPRESS_DISABLE_GUEST_TELEMETRY, która wyłącza raportowanie wysyłane dla sesji bez zalogowania. Sam fakt, że dodano ją dopiero teraz, mówi, że wcześniej anonimowe zdarzenia z trybu otwartego i z wiersza poleceń wychodziły na zewnątrz domyślnie.
Instalacja i konfiguracja
Wejście do projektu wygląda tak.
# instalacja z przypięciem dokładnej wersji
npm install --save-dev --save-exact cypress
# okno z dziennikiem poleceń, kreator konfiguracji przy pierwszym uruchomieniu
npx cypress open
# przebieg bez okna, do potoku ciągłej integracji
npx cypress run
# wybrana przeglądarka i pojedyncza specyfikacja
npx cypress run --browser firefox --spec "cypress/e2e/logowanie.cy.ts"
# testy komponentów zamiast end to end
npx cypress run --component
# przebieg zapisywany w Cypress Cloud, z podziałem na maszyny
npx cypress run --record --key "$CYPRESS_RECORD_KEY" --parallel \
--ci-build-id "$GITHUB_RUN_ID" --group "e2e-chrome"
# sprawdzenie, czy pobrane binarium w ogóle się uruchamia
npx cypress verify
# dostęp agenta do otwartej sesji, dodany w 15.21.0
npx cypress tap --helpKonfiguracja siedzi w pliku cypress.config.ts w katalogu głównym. Poniżej zestaw pól, które faktycznie istnieją w definicjach typów wersji 15.21.0.
import { defineConfig } from 'cypress'
export default defineConfig({
defaultCommandTimeout: 8000,
pageLoadTimeout: 60000,
requestTimeout: 5000,
responseTimeout: 30000,
viewportWidth: 1280,
viewportHeight: 800,
video: false,
videoCompression: false,
screenshotOnRunFailure: true,
trashAssetsBeforeRuns: true,
numTestsKeptInMemory: 20,
experimentalMemoryManagement: true,
retries: { runMode: 2, openMode: 0 },
blockHosts: ['*.google-analytics.com', '*.hotjar.com'],
e2e: {
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
supportFile: 'cypress/support/e2e.ts',
testIsolation: true,
setupNodeEvents(on, config) {
on('task', {
resetDatabase: async () => {
await fetch(`${config.env.apiUrl}/test/reset`, { method: 'POST' })
return null
}
})
return config
}
},
component: {
devServer: { framework: 'react', bundler: 'vite' },
specPattern: 'src/**/*.cy.{ts,tsx}'
}
})Kilka pól zasługuje na komentarz. retries przyjmuje osobne wartości dla trybu wsadowego i dla okna, i ustawianie ponowień w trybie otwartym zwykle nie ma sensu, bo maskuje błąd, który właśnie próbujesz obejrzeć. numTestsKeptInMemory domyślnie wynosi 50 i to jest jeden z głównych powodów, dla których przeglądarka puchnie w długich specyfikacjach; obniżenie tej wartości kosztuje migawki starszych testów. experimentalMemoryManagement bywa konieczne w kontenerach z twardym limitem pamięci. Domyślna wartość videoCompression to false, mimo że definicje typów podawały do niedawna błędnie 32.
Pętla debugowania, czyli najmocniejsza strona narzędzia
Sam test wygląda zwyczajnie, ale znaczenie ma to, co się dzieje po jego wykonaniu.
describe('koszyk', () => {
beforeEach(() => {
cy.task('resetDatabase')
cy.visit('/sklep')
})
it('dodaje produkt i przelicza sumę', () => {
cy.intercept('POST', '/api/cart', { statusCode: 201, body: { items: 1 } }).as('addToCart')
cy.get('[data-cy=product-card]').first().within(() => {
cy.get('[data-cy=add-to-cart]').click()
})
cy.wait('@addToCart').its('request.body').should('deep.equal', { sku: 'CW-001', qty: 1 })
cy.get('[data-cy=cart-total]')
.should('be.visible')
.and('contain.text', '129,00')
cy.window().its('localStorage.cartId').should('be.a', 'string')
})
})Po zakończeniu tego testu w oknie Cypressa masz listę czterech kroków. Klikasz cy.wait('@addToCart') i w konsoli przeglądarki wypisuje się pełny obiekt żądania oraz odpowiedzi. Najeżdżasz na .click() i podgląd wraca do stanu sprzed kliknięcia. Przypinasz krok, przełączasz się na narzędzia deweloperskie i normalnie grzebiesz w modelu dokumentu z tego momentu. Nic z tego nie wymaga ponownego przebiegu.
Polecenie cy.window() pokazuje przy okazji, na czym polega przewaga wykonywania kodu w przeglądarce. Sięgasz po prawdziwy obiekt okna aplikacji, a nie po jego odwzorowanie przesłane protokołem. Możesz z niego czytać stan magazynu Redux, podmienić metodę na obiekcie globalnym albo wywołać funkcję wystawioną przez aplikację. W narzędziach sterujących przeglądarką z zewnątrz to samo wymaga przekazania funkcji do wykonania po drugiej stronie i przyjęcia z powrotem tylko wartości dających się serializować.
Od wersji 15.21.0 doszło polecenie cypress tap, które wystawia otwartą sesję agentowi. Podkomendy wypisują listę działających sesji, uruchamiają i powtarzają specyfikację, raportują wynik przebiegu, drukują błąd nieudanego testu razem z dziennikiem poleceń oraz pokazują model dokumentu i drzewo dostępności badanej aplikacji. Każda drukuje czytelny tekst, a z flagą --json dane do maszynowego odczytu. Dla kogoś, kto pisze testy z GitHub Copilot albo z innym asystentem, to konkretna zmiana, bo agent przestaje zgadywać, dlaczego test upadł.
Logowanie, sieć i wiele domen
Trzy polecenia rozstrzygają o tym, czy zestaw testów będzie szybki, czy nie.
// logowanie raz na całą specyfikację, a nie przed każdym testem
const login = (email: string, password: string) => {
cy.session([email, password], () => {
cy.visit('/login')
cy.get('[data-cy=email]').type(email)
cy.get('[data-cy=password]').type(password, { log: false })
cy.get('[data-cy=submit]').click()
cy.url().should('include', '/panel')
}, {
cacheAcrossSpecs: true,
validate: () => {
cy.request('/api/me').its('status').should('eq', 200)
}
})
}
// logowanie przez zewnętrznego dostawcę tożsamości
it('loguje się przez dostawcę zewnętrznego', () => {
cy.visit('/login')
cy.get('[data-cy=sso]').click()
cy.origin('https://accounts.example.com', { args: { user: 'ala@example.com' } }, ({ user }) => {
cy.get('input[name=identifier]').type(user)
cy.get('button[type=submit]').click()
})
cy.url().should('include', '/panel')
})cy.session() zapisuje ciasteczka oraz zawartość localStorage i sessionStorage, a przy kolejnym wywołaniu z tym samym identyfikatorem odtwarza je zamiast przechodzić przez formularz. Opcja cacheAcrossSpecs domyślnie ma wartość false, więc bez niej pamięć podręczna sesji kończy się razem ze specyfikacją. Funkcja validate sprawdza, czy odtworzona sesja nadal jest ważna, i przy niepowodzeniu Cypress buduje ją od nowa. Bez tego mechanizmu każdy test loguje się formularzem i zestaw testów robi się kilkukrotnie wolniejszy.
cy.origin() to obejście ograniczenia jednej nadrzędnej domeny. Blok przekazany do tego polecenia jest wykonywany w kontekście innej domeny, ale jest to osobny świat: nie widzi zmiennych z otoczenia, więc wszystko trzeba przekazać przez args, a wartości muszą dać się serializować. Import modułu w środku bloku wymaga włączenia experimentalOriginDependencies. Do jednego przepływu logowania to wystarcza. Do testu, który skacze między trzema domenami i porównuje ich stan, zapis robi się ciężki.
cy.intercept() przechwytuje żądania sieciowe i pozwala je podmienić albo tylko obserwować. Połączenia WebSocket Cypress przepuszcza przez pośrednika i widzi żądanie zmiany protokołu jako resourceType: 'websocket', ale podmiana pojedynczych ramek nie jest obsługiwana. Jeśli testujesz czat albo powiadomienia na żywo, to jest granica narzędzia.
Cypress Cloud, czyli za co się płaci
Samo narzędzie jest darmowe i bez ograniczeń. Płaci się za usługę, która przyjmuje wyniki przebiegów, i to ona rozstrzyga o dwóch rzeczach ważnych w potoku: o zrównolegleniu i o odtwarzaniu przebiegów. Flagi --parallel i --record wymagają klucza projektu, bo to serwer Cypress Cloud rozdziela specyfikacje między maszyny i równoważy obciążenie na podstawie historii czasów.
Wbrew rozpowszechnionej opinii zrównoleglenie i Test Replay są dostępne także w planie darmowym. Ograniczeniem nie jest funkcja, tylko licznik. Plan Starter obejmuje 50 użytkowników, 500 wyników testów miesięcznie, 100 wywołań generatora testów miesięcznie, 30 dni przechowywania danych i wsparcie wyłącznie społecznościowe. Wynik testu to pojedynczy test w pojedynczym przebiegu, więc zestaw 60 testów uruchamiany przy każdym scaleniu zużyje ten limit po niespełna dziewięciu przebiegach. Dla jednoosobowego projektu to wystarczy, dla zespołu nie.
Cennik wyższych planów wygląda tak, jak podaje strona cennika. Plan Team kosztuje 799 dolarów rocznie i obejmuje 120 tysięcy wyników testów rocznie, 9 tysięcy wywołań generatora, 90 dni przechowywania danych oraz wykrywanie testów niestabilnych i integrację z Jirą. Plan Business kosztuje 3199 dolarów rocznie, ma te same 120 tysięcy wyników, 24 tysiące wywołań generatora, priorytetyzację specyfikacji, automatyczne przerywanie przebiegu i logowanie jednokrotne. Enterprise ma cenę negocjowaną, nieograniczoną liczbę użytkowników, 1,8 miliona wyników rocznie i 180 dni przechowywania danych.
Dwie rzeczy w tym cenniku wymagają zaznaczenia. Karty planów pokazują kwoty miesięczne 67 i 267 dolarów, ale to jest cena roczna podzielona przez dwanaście i zaokrąglona w górę, bo 799 dzielone przez dwanaście daje 66,58, a 3199 daje 266,58. Iloczyn kwoty miesięcznej i dwunastu nie zgadza się z kwotą roczną i nie jest to osobna oferta. Druga rzecz to koszt przekroczenia limitu: wyniki dokupowane z góry kosztują 5,28 dolara za tysiąc w planie Team, 4,40 w Business i 4,00 w Enterprise, a wyniki rozliczane po fakcie odpowiednio 6 i 5 dolarów za tysiąc. To jest miejsce, w którym rachunek potrafi urosnąć niezauważenie, bo liczy się każde powtórzenie nieudanego testu.
Alternatywą jest zrównoleglenie własnymi siłami: podział specyfikacji na maszyny przez macierz zadań w systemie ciągłej integracji i przekazywanie zakresu przez --spec. Traci się wtedy równoważenie obciążenia na podstawie historii, a raport trzeba złożyć z osobnych plików. Dla stu specyfikacji na czterech maszynach to działa i nie kosztuje nic.
Cypress a alternatywy
| Cecha | Cypress | Playwright | Selenium WebDriver | WebdriverIO | Puppeteer |
|---|---|---|---|---|---|
| Gdzie działa kod testu | w przeglądarce | w Node | w Node | w Node | w Node |
| Języki testów | JavaScript i TypeScript | JS, TS, Python, Java, .NET | wiele języków | JavaScript i TypeScript | JavaScript i TypeScript |
| Wiele kart w teście | przez wtyczkę @cypress/puppeteer | natywnie | natywnie | natywnie | natywnie |
| Wiele domen | przez cy.origin | natywnie | natywnie | natywnie | natywnie |
| Zrównoleglenie | wymaga Cypress Cloud | wbudowane, opcja workers | przez Selenium Grid | wbudowane, opcja maxInstances | własna implementacja |
| Silniki przeglądarek | Chromium i Firefox, WebKit eksperymentalnie | Chromium, Firefox, WebKit | zależy od sterownika | zależy od sterownika | Chromium i Firefox |
| Bieżąca wersja | 15.21.0 | 1.62.1 | 4.47.0 | 9.31.2 | 25.8.0 |
| Licencja pakietu npm | MIT | Apache 2.0 | Apache 2.0 | MIT | Apache 2.0 |
Wybór rozstrzyga się na kilku pytaniach. Jeśli testowany przepływ dotyka wielu kart, wielu domen albo wielu równoczesnych użytkowników, na przykład czatu albo dokumentu edytowanego we dwoje, Playwright wygrywa bez dyskusji i nie ma tu czego naciągać. Jeśli zespół potrzebuje pisać testy w Pythonie albo w Javie, Cypress w ogóle nie jest kandydatem. Jeśli zrównoleglenie w potoku ma być wbudowane i darmowe, Playwright też jest prostszym wyborem.
Cypress nadal wygrywa tam, gdzie liczy się czas wchodzenia do narzędzia i czas znajdowania przyczyny błędu. Migawki stanu po każdym poleceniu, jedno okno zamiast raportu w przeglądarce, brak potrzeby rozumienia asynchroniczności w JavaScripcie przy pisaniu pierwszych testów. Dla zespołu, w którym testy pisze także ktoś spoza rdzenia deweloperskiego, ta różnica jest odczuwalna. Do tego dochodzą testy komponentów w prawdziwej przeglądarce, sensownie łączące się z Reactem i z katalogiem komponentów w Storybook, gdzie renderowanie w pamięci nie wystarcza.
Typowe błędy
Pierwszy to używanie cy.wait() z liczbą milisekund. Skoro polecenia i tak ponawiają zapytanie do skutku, sztywne czekanie tylko wydłuża przebieg i nadal nie gwarantuje, że warunek zaszedł. Poprawną formą jest cy.wait('@alias') po przechwyceniu żądania albo asercja, która sama się ponawia.
Drugi to traktowanie wyniku polecenia jak zwykłej wartości. const el = cy.get('.item') nie zwraca elementu, tylko obiekt kolejki. Wartość dostajesz w .then() albo przez cy.wrap(). Mieszanie async i await z łańcuchem poleceń kończy się testem, który przechodzi z niewłaściwego powodu.
Trzeci to selektory oparte na klasach CSS albo na treści przycisku. Klasa zmieni się przy najbliższym przemeblowaniu stylów, a treść przy zmianie tłumaczenia. Atrybut data-cy jest brzydki w kodzie i tani w utrzymaniu, i to jest właściwy kompromis.
Czwarty to logowanie formularzem przed każdym testem. Zamiast tego cy.session() z cacheAcrossSpecs: true i z funkcją validate. Bez cacheAcrossSpecs pamięć podręczna kończy się razem z plikiem specyfikacji, co przy stu plikach oznacza sto logowań.
Piąty to wyłączanie testIsolation po to, żeby testy zależały od siebie po kolei. Pole istnieje i domyślnie ma wartość true, ale ustawienie go na false daje zestaw, w którym awaria trzeciego testu przewraca wszystkie kolejne, a przyczyna jest nie do odczytania z raportu.
Szósty to ponowienia jako lekarstwo na niestabilność. retries: { runMode: 2 } ukrywa problem i podwaja rachunek w Cypress Cloud, bo każde powtórzenie liczy się jako osobny wynik testu. Ponowienia mają sens jako siatka bezpieczeństwa, a nie jako sposób utrzymania zielonego potoku.
Siódmy to brak buforowania binarium w potoku. Pakiet z npm pobiera plik wykonywalny przy każdej instalacji, więc bez zapisania katalogu wskazanego przez CYPRESS_CACHE_FOLDER każdy przebieg zaczyna się od ściągnięcia kilkuset megabajtów z serwera dostawcy.
Ósmy to niepodpisanie w konfiguracji ról typów. Definicje typów są w pakiecie, ale bez "types": ["cypress"] w tsconfig.json edytor nie zna cy ani Cypress, co w projekcie na TypeScripcie kasuje połowę korzyści z podpowiedzi.
FAQ
Czy Cypress jest darmowy?
Samo narzędzie tak, na licencji MIT, bez ograniczeń w użyciu komercyjnym i bez limitu przebiegów lokalnych oraz w potoku. Płatna jest usługa Cypress Cloud, przy czym plan Starter jest bezpłatny i obejmuje 500 wyników testów miesięcznie oraz 30 dni przechowywania danych. Wyższe plany kosztują 799 i 3199 dolarów rocznie.
Czy zrównoleglenie wymaga płatnego planu?
Nie wymaga planu płatnego, ale wymaga konta w Cypress Cloud, bo to serwer usługi rozdziela specyfikacje między maszyny. W planie darmowym mieści się w limicie 500 wyników miesięcznie. Alternatywą bez usługi jest własny podział przez --spec i macierz zadań w systemie ciągłej integracji, kosztem utraty równoważenia obciążenia.
Czy Cypress obsługuje wiele kart i wiele domen?
Wiele kart nie w sposób natywny; dokumentacja wskazuje wtyczkę @cypress/puppeteer w wersji 0.1.8. Wiele domen działa przez cy.origin(), gdzie blok kodu jest wykonywany w osobnym kontekście, argumenty przekazujesz przez args i muszą dać się serializować. To wystarcza do logowania przez dostawcę zewnętrznego, ale nie do rozbudowanych scenariuszy międzydomenowych.
Kiedy wybrać Playwright zamiast Cypressa?
Gdy potrzebujesz wielu kart, wielu równoczesnych użytkowników, testów w Pythonie lub Javie, wbudowanego zrównoleglenia bez usługi zewnętrznej albo pełnego wsparcia dla silnika WebKit, który w Cypressie jest wciąż oznaczony jako eksperymentalny. Do prostych przepływów w jednej domenie różnica jest niewielka i wtedy decyduje wygoda debugowania.
Czy paczka z npm zawiera plik licencyjny?
Nie. Repozytorium ma plik LICENSE z tekstem MIT, rejestr npm podaje MIT, ale w opublikowanym archiwum wersji 15.21.0 nie ma żadnego pliku licencyjnego. Dodatkowo właściwe binarium nie jest publikowane na npm, tylko pobierane ze strony dostawcy przez skrypt postinstall.
Czy Cypress zastępuje testy jednostkowe?
Nie i nie warto go do tego naginać. Testy komponentów w Cypressie renderują komponent w prawdziwej przeglądarce, co ma sens przy sprawdzaniu stylów, zdarzeń wejściowych i dostępności. Do szybkich testów logiki bez modelu dokumentu narzędzie uruchamiane w Node jest o rząd wielkości szybsze.
Dokumentację znajdziesz na stronie dokumentacji Cypressa, listę ograniczeń trwałych w dziale kompromisów, a kod źródłowy w repozytorium na GitHubie.