Frontend Testing - Jest, React Testing Library, Cypress [2026]
Testowanie frontend to jeden z obszarów, gdzie wielu developerów ma luki. Na rozmowach rekrutacyjnych pytania o testy pojawiają się coraz częściej - szczególnie w dojrzałych organizacjach dbających o jakość kodu. Ten przewodnik zawiera 50+ pytań z odpowiedziami - od podstaw Jest, przez React Testing Library, po testy E2E w Cypress.
Spis treści
- Podstawy testowania frontend
- Jest - framework testowy
- Jest - mockowanie
- React Testing Library
- RTL - queries i interakcje
- Cypress - testy E2E
- Best practices
- Zobacz też
Podstawy testowania frontend
Jakie są główne rodzaje testów w aplikacjach frontend?
Odpowiedź w 30 sekund: Trzy główne rodzaje: Unit tests (pojedyncze funkcje/komponenty), Integration tests (współpraca komponentów), E2E tests (cała aplikacja jak użytkownik). Piramida testów zaleca: dużo unit, średnio integration, mało e2e.
Odpowiedź w 2 minuty:
Piramida testów wizualizuje optymalny rozkład testów w aplikacji - od najszybszych i najtańszych na dole, po najwolniejsze i najdroższe na górze.
/\
/ \
/ E2E \ ← Mało (wolne, drogie, kruche)
/--------\ Cypress, Playwright
/ \
/ Integration \ ← Średnio
/--------------\ React Testing Library
/ \
/ Unit Tests \ ← Dużo (szybkie, tanie, stabilne)
/--------------------\ Jest
| Typ | Narzędzie | Co testuje | Szybkość | Koszt |
|---|---|---|---|---|
| Unit | Jest | Pojedyncze funkcje, hooki | Bardzo szybkie | Niski |
| Integration | RTL | Komponenty + interakcje | Szybkie | Średni |
| E2E | Cypress | Cała aplikacja w przeglądarce | Wolne | Wysoki |
// UNIT TEST - testuje pojedynczą funkcję
test('formatPrice formats correctly', () => {
expect(formatPrice(1999)).toBe('19,99 zł')
expect(formatPrice(0)).toBe('0,00 zł')
})
// INTEGRATION TEST - testuje komponent z interakcjami
test('user can add item to cart', async () => {
render(<ProductPage />)
await userEvent.click(screen.getByRole('button', { name: /dodaj/i }))
expect(screen.getByText('Dodano do koszyka')).toBeInTheDocument()
})
// E2E TEST - testuje całą ścieżkę użytkownika
it('user can complete purchase', () => {
cy.visit('/products')
cy.contains('Dodaj do koszyka').click()
cy.contains('Przejdź do kasy').click()
cy.get('[data-cy=email]').type('test@example.com')
cy.contains('Zamów').click()
cy.contains('Dziękujemy za zamówienie')
})
Czym jest piramida testów i jak ją stosować?
Odpowiedź w 30 sekund: Piramida testów to strategia mówiąca, że powinniśmy mieć dużo szybkich unit testów na dole, średnio integration testów w środku, i mało wolnych E2E testów na górze. Im wyżej, tym wolniejsze i droższe testy.
Odpowiedź w 2 minuty:
Oto przykładowy rozkład testów dla aplikacji React z podziałem procentowym pokazującym, jak stosować piramidę w praktyce.
// Przykładowy rozkład dla aplikacji React:
// 70% - UNIT TESTS (Jest)
// - Utility functions (formatters, validators)
// - Custom hooks
// - Reducery/slice'y Redux
// - Pure components
test('validateEmail returns true for valid email', () => {
expect(validateEmail('test@example.com')).toBe(true)
expect(validateEmail('invalid')).toBe(false)
})
// 20% - INTEGRATION TESTS (RTL)
// - Komponenty z interakcjami
// - Formularze
// - Flows wewnątrz strony
test('login form shows error for invalid credentials', async () => {
render(<LoginForm />)
await userEvent.type(screen.getByLabelText(/email/i), 'wrong@test.com')
await userEvent.type(screen.getByLabelText(/hasło/i), 'wrongpass')
await userEvent.click(screen.getByRole('button', { name: /zaloguj/i }))
expect(await screen.findByText(/nieprawidłowe dane/i)).toBeInTheDocument()
})
// 10% - E2E TESTS (Cypress)
// - Krytyczne ścieżki użytkownika
// - Checkout flow
// - Rejestracja/logowanie
// - Integracje z zewnętrznymi serwisami
it('user can complete full checkout flow', () => {
cy.login('user@test.com', 'password')
cy.visit('/products')
cy.addToCart('Product 1')
cy.checkout()
cy.contains('Zamówienie złożone')
})
Anty-wzorzec: Ice Cream Cone
Dużo E2E (wolne, kruche)
/ \
/ Średnio Integration \
/ \
/ Mało Unit \
/________________________________\
❌ Odwrócona piramida = wolne testy, trudne utrzymanie
Jest - framework testowy
Czym jest Jest i jak go skonfigurować?
Odpowiedź w 30 sekund:
Jest to framework testowy JavaScript od Meta. Zero konfiguracji dla projektów Create React App. Dla innych projektów: npm install -D jest @types/jest ts-jest i stwórz jest.config.js. Oferuje: asercje, mockowanie, snapshot testing, code coverage.
Odpowiedź w 2 minuty:
Poniżej znajduje się przykład pełnej konfiguracji Jest dla projektu TypeScript z React, uwzględniającej wszystkie najważniejsze opcje.
// jest.config.js
module.exports = {
// Preset dla TypeScript
preset: 'ts-jest',
// Środowisko - jsdom dla testów React
testEnvironment: 'jsdom',
// Setup przed testami (np. importy RTL)
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
// Ścieżki do plików testowych
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
// Ignorowane foldery
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
// Mapowanie modułów (aliasy)
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': 'identity-obj-proxy'
},
// Pokrycie kodu
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
// src/setupTests.ts
import '@testing-library/jest-dom'
Struktura testu:
// math.test.ts
describe('Math utilities', () => {
describe('add', () => {
it('adds two positive numbers', () => {
expect(add(2, 3)).toBe(5)
})
it('handles negative numbers', () => {
expect(add(-1, 5)).toBe(4)
})
})
})
// Uruchamianie
// npm test - watch mode
// npm test -- --coverage - z pokryciem
// npm test -- --watch=false - single run (CI)
Jakie są najczęściej używane matchery w Jest?
Odpowiedź w 30 sekund:
Podstawowe: toBe (===), toEqual (deep equal), toBeTruthy/Falsy, toBeNull/Undefined. Dla liczb: toBeGreaterThan, toBeCloseTo. Dla stringów: toMatch, toContain. Dla tablic: toContain, toHaveLength. Dla obiektów: toHaveProperty.
Odpowiedź w 2 minuty:
Oto kompletna lista najczęściej używanych matcherów Jest z przykładami użycia dla różnych typów danych i scenariuszy testowych.
// EQUALITY
expect(1 + 1).toBe(2) // Strict equality (===)
expect({ a: 1 }).toEqual({ a: 1 }) // Deep equality
expect({ a: 1 }).not.toBe({ a: 1 }) // Różne referencje!
// TRUTHINESS
expect(true).toBeTruthy()
expect(0).toBeFalsy()
expect(null).toBeNull()
expect(undefined).toBeUndefined()
expect('value').toBeDefined()
// NUMBERS
expect(10).toBeGreaterThan(5)
expect(10).toBeGreaterThanOrEqual(10)
expect(5).toBeLessThan(10)
expect(0.1 + 0.2).toBeCloseTo(0.3) // Floating point!
// STRINGS
expect('Hello World').toMatch(/World/)
expect('Hello World').toContain('World')
// ARRAYS
expect([1, 2, 3]).toContain(2)
expect([1, 2, 3]).toHaveLength(3)
expect([1, 2, 3]).toEqual(expect.arrayContaining([2, 3]))
// OBJECTS
expect({ a: 1, b: 2 }).toHaveProperty('a')
expect({ a: 1, b: 2 }).toHaveProperty('a', 1)
expect({ a: 1 }).toEqual(expect.objectContaining({ a: 1 }))
// EXCEPTIONS
expect(() => { throw new Error('fail') }).toThrow()
expect(() => { throw new Error('fail') }).toThrow('fail')
expect(() => { throw new Error('fail') }).toThrow(Error)
// ASYNC
await expect(asyncFn()).resolves.toBe('success')
await expect(asyncFn()).rejects.toThrow('error')
// NEGATION
expect(1).not.toBe(2)
expect([]).not.toContain(1)
Jest - mockowanie
Jak mockować funkcje i moduły w Jest?
Odpowiedź w 30 sekund:
jest.fn() tworzy mock funkcji. jest.mock('module') mockuje cały moduł. jest.spyOn(obj, 'method') śledzi wywołania istniejącej metody. Mocki pozwalają: śledzić wywołania, kontrolować return value, izolować testy.
Odpowiedź w 2 minuty:
Mockowanie w Jest odbywa się na trzech głównych poziomach - funkcje, moduły i metody obiektów. Oto przykłady dla każdego z nich.
// MOCK FUNKCJI - jest.fn()
const mockFn = jest.fn()
mockFn('arg1', 'arg2')
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
expect(mockFn).toHaveBeenCalledTimes(1)
// Mock z return value
const mockFn = jest.fn().mockReturnValue(42)
expect(mockFn()).toBe(42)
// Mock z implementacją
const mockFn = jest.fn((x) => x * 2)
expect(mockFn(5)).toBe(10)
// Mock async
const mockAsync = jest.fn().mockResolvedValue('data')
await expect(mockAsync()).resolves.toBe('data')
// MOCK MODUŁU - jest.mock()
// __mocks__/api.ts lub inline
jest.mock('./api', () => ({
fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' }),
saveUser: jest.fn().mockResolvedValue({ success: true })
}))
import { fetchUser } from './api'
test('uses mocked api', async () => {
const user = await fetchUser(1)
expect(user.name).toBe('John')
expect(fetchUser).toHaveBeenCalledWith(1)
})
// SPY - jest.spyOn()
const user = {
getName: () => 'Original'
}
const spy = jest.spyOn(user, 'getName').mockReturnValue('Mocked')
expect(user.getName()).toBe('Mocked')
expect(spy).toHaveBeenCalled()
spy.mockRestore() // Przywróć oryginał
expect(user.getName()).toBe('Original')
Jak mockować fetch i wywołania API?
Odpowiedź w 30 sekund:
Mockuj global.fetch za pomocą jest.fn() lub użyj bibliotek jak msw (Mock Service Worker). Zawsze resetuj mocki w afterEach. Dla RTL lepsze jest mockowanie na poziomie warstwy API niż global fetch.
Odpowiedź w 2 minuty:
Istnieją trzy główne sposoby mockowania API - bezpośredni mock fetch, mockowanie warstwy API, oraz użycie MSW. Każdy ma swoje zastosowania.
// Sposób 1: Mock global.fetch
beforeEach(() => {
global.fetch = jest.fn()
})
afterEach(() => {
jest.restoreAllMocks()
})
test('fetches user data', async () => {
const mockUser = { id: 1, name: 'John' }
;(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockUser)
})
const user = await fetchUser(1)
expect(fetch).toHaveBeenCalledWith('/api/users/1')
expect(user).toEqual(mockUser)
})
test('handles fetch error', async () => {
;(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
status: 404
})
await expect(fetchUser(999)).rejects.toThrow('User not found')
})
// Sposób 2: Mock modułu API (lepszy)
// api.ts
export const api = {
getUser: (id: number) => fetch(`/api/users/${id}`).then(r => r.json())
}
// component.test.tsx
jest.mock('./api', () => ({
api: {
getUser: jest.fn()
}
}))
import { api } from './api'
test('UserProfile displays user name', async () => {
;(api.getUser as jest.Mock).mockResolvedValue({ name: 'John' })
render(<UserProfile userId={1} />)
expect(await screen.findByText('John')).toBeInTheDocument()
})
// Sposób 3: MSW (Mock Service Worker) - najbardziej realistyczny
import { rest } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: req.params.id, name: 'John' }))
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('fetches and displays user', async () => {
render(<UserProfile userId={1} />)
expect(await screen.findByText('John')).toBeInTheDocument()
})
Jak mockować timery (setTimeout, setInterval)?
Odpowiedź w 30 sekund:
jest.useFakeTimers() włącza fake timery. jest.advanceTimersByTime(ms) przesuwa czas. jest.runAllTimers() wykonuje wszystkie timery. Pamiętaj o jest.useRealTimers() w cleanup.
Odpowiedź w 2 minuty:
Fake timery w Jest pozwalają kontrolować przepływ czasu w testach. Oto przykład testowania funkcji debounce z użyciem fake timerów.
// Funkcja z debounce
function debounce(fn: Function, delay: number) {
let timeoutId: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}
}
describe('debounce', () => {
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
})
test('calls function after delay', () => {
const fn = jest.fn()
const debouncedFn = debounce(fn, 500)
debouncedFn('arg1')
// Funkcja nie została jeszcze wywołana
expect(fn).not.toHaveBeenCalled()
// Przesuń czas o 500ms
jest.advanceTimersByTime(500)
// Teraz została wywołana
expect(fn).toHaveBeenCalledWith('arg1')
})
test('resets timer on subsequent calls', () => {
const fn = jest.fn()
const debouncedFn = debounce(fn, 500)
debouncedFn('first')
jest.advanceTimersByTime(300)
debouncedFn('second') // Reset timer
jest.advanceTimersByTime(300)
// Jeszcze nie wywołana (tylko 300ms od ostatniego)
expect(fn).not.toHaveBeenCalled()
jest.advanceTimersByTime(200)
// Teraz wywołana z drugim argumentem
expect(fn).toHaveBeenCalledWith('second')
expect(fn).toHaveBeenCalledTimes(1)
})
})
// Dla komponentów React z timerami
test('auto-saves after 3 seconds of inactivity', async () => {
jest.useFakeTimers()
const onSave = jest.fn()
render(<AutoSaveForm onSave={onSave} />)
await userEvent.type(screen.getByRole('textbox'), 'text')
expect(onSave).not.toHaveBeenCalled()
// Przesuń czas - React wymaga act()
act(() => {
jest.advanceTimersByTime(3000)
})
expect(onSave).toHaveBeenCalled()
jest.useRealTimers()
})
React Testing Library
Czym jest React Testing Library i jaka jest jej filozofia?
Odpowiedź w 30 sekund: RTL testuje komponenty jak użytkownik - przez tekst, role, labele, nie przez implementację. "The more your tests resemble the way your software is used, the more confidence they give you." Promuje testy odporne na refaktoryzację.
Odpowiedź w 2 minuty:
Porównanie podejścia Enzyme i React Testing Library pokazuje kluczową różnicę w filozofii testowania - implementacja vs zachowanie.
// ❌ ENZYME - testuje implementację
const wrapper = shallow(<Counter />)
expect(wrapper.state('count')).toBe(0)
wrapper.instance().increment()
expect(wrapper.state('count')).toBe(1)
// Problem: Test się zepsuje przy refaktoryzacji
// (np. useState zamiast class state)
// ✅ RTL - testuje zachowanie
render(<Counter />)
// Użytkownik widzi "0"
expect(screen.getByText('0')).toBeInTheDocument()
// Użytkownik klika przycisk "+"
await userEvent.click(screen.getByRole('button', { name: '+' }))
// Użytkownik widzi "1"
expect(screen.getByText('1')).toBeInTheDocument()
// Test działa niezależnie od implementacji (class/hooks/zustand)
Hierarchia queries - od najlepszej do najgorszej:
// 1. ACCESSIBLE - dostępne dla wszystkich
getByRole('button', { name: /submit/i }) // ← Najlepsza
getByLabelText(/email/i)
getByPlaceholderText('Enter email')
getByText(/welcome/i)
getByDisplayValue('current value')
// 2. SEMANTIC - semantyczne HTML
getByAltText('profile picture')
getByTitle('Close')
// 3. TEST ID - ostateczność
getByTestId('submit-button') // ← Używaj gdy nie ma innej opcji
Dlaczego getByRole jest najlepsza?
// Sprawdza jednocześnie:
// 1. Element istnieje
// 2. Ma odpowiedni role (semantyka)
// 3. Jest dostępny (accessibility)
// 4. Ma odpowiednią nazwę
getByRole('button', { name: /zapisz/i })
// Znajdzie: <button>Zapisz</button>
// Znajdzie: <button aria-label="Zapisz"><Icon /></button>
// NIE znajdzie: <div onClick={...}>Zapisz</div> (brak role!)
RTL - queries i interakcje
Jaka jest różnica między getBy, queryBy i findBy?
Odpowiedź w 30 sekund:
getBy - rzuca błąd jeśli nie znajdzie (dla elementów które MUSZĄ istnieć). queryBy - zwraca null jeśli nie znajdzie (testowanie nieobecności). findBy - czeka asynchronicznie (Promise), dla elementów pojawiających się po czasie.
Odpowiedź w 2 minuty:
Każdy typ query ma swoje specyficzne zastosowanie - wybór zależy od tego, czy element musi istnieć, czy może go nie być, lub czy pojawia się asynchronicznie.
// getBy - SYNCHRONICZNY, RZUCA BŁĄD
// Użyj gdy element MUSI istnieć
test('shows title', () => {
render(<Page />)
expect(screen.getByText('Welcome')).toBeInTheDocument()
// Jeśli nie znajdzie - test FAILED natychmiast
})
// queryBy - SYNCHRONICZNY, ZWRACA NULL
// Użyj do testowania NIEOBECNOŚCI elementu
test('does not show error initially', () => {
render(<Form />)
expect(screen.queryByText('Error')).not.toBeInTheDocument()
// lub
expect(screen.queryByText('Error')).toBeNull()
})
// findBy - ASYNCHRONICZNY (Promise)
// Użyj dla elementów pojawiających się PO CZASIE
test('shows data after fetch', async () => {
render(<UserProfile />)
// Loader jest od razu
expect(screen.getByText('Loading...')).toBeInTheDocument()
// User pojawia się po fetchu
expect(await screen.findByText('John Doe')).toBeInTheDocument()
// findBy domyślnie czeka 1000ms (można zmienić)
expect(await screen.findByText('John Doe', {}, { timeout: 3000 }))
})
// WSZYSTKIE WARIANTY
// Single element:
getByRole / queryByRole / findByRole
getByText / queryByText / findByText
getByLabelText / queryByLabelText / findByLabelText
// Multiple elements:
getAllByRole / queryAllByRole / findAllByRole
// getAllBy rzuca błąd jeśli znajdzie 0
// queryAllBy zwraca [] jeśli znajdzie 0
| Metoda | Nie znaleziono | Znaleziono 1 | Znaleziono >1 | Async |
|---|---|---|---|---|
| getBy | Błąd | Element | Błąd | Nie |
| queryBy | null | Element | Błąd | Nie |
| findBy | Błąd (po timeout) | Element | Błąd | Tak |
| getAllBy | Błąd | [Element] | [Elements] | Nie |
| queryAllBy | [] | [Element] | [Elements] | Nie |
| findAllBy | Błąd (po timeout) | [Element] | [Elements] | Tak |
Jak symulować interakcje za pomocą userEvent?
Odpowiedź w 30 sekund:
userEvent symuluje realistyczne interakcje użytkownika (klawiatura, focus, itp.). Jest lepszy niż fireEvent bo wyzwala pełną sekwencję zdarzeń. Zawsze używaj await - userEvent jest asynchroniczny od v14.
Odpowiedź w 2 minuty:
Biblioteka userEvent oferuje szeroki wachlarz metod do symulowania interakcji użytkownika - od podstawowych kliknięć po zaawansowaną nawigację klawiaturą.
import userEvent from '@testing-library/user-event'
test('form submission flow', async () => {
// Setup userEvent
const user = userEvent.setup()
const onSubmit = jest.fn()
render(<LoginForm onSubmit={onSubmit} />)
// Kliknięcie
await user.click(screen.getByRole('button', { name: /zaloguj/i }))
// Wpisywanie tekstu (symuluje keydown, keypress, keyup dla każdego znaku)
await user.type(screen.getByLabelText(/email/i), 'test@example.com')
await user.type(screen.getByLabelText(/hasło/i), 'password123')
// Czyszczenie pola
await user.clear(screen.getByLabelText(/email/i))
// Tab (nawigacja klawiaturą)
await user.tab()
expect(screen.getByLabelText(/hasło/i)).toHaveFocus()
// Keyboard shortcuts
await user.keyboard('{Enter}') // Submit formularza
await user.keyboard('{Escape}')
await user.keyboard('{Control>}a{/Control}') // Ctrl+A
// Hover
await user.hover(screen.getByText('Tooltip trigger'))
expect(screen.getByRole('tooltip')).toBeVisible()
await user.unhover(screen.getByText('Tooltip trigger'))
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
// Zaznaczanie checkbox/radio
await user.click(screen.getByLabelText(/remember me/i))
expect(screen.getByLabelText(/remember me/i)).toBeChecked()
// Select dropdown
await user.selectOptions(
screen.getByRole('combobox'),
screen.getByRole('option', { name: 'Poland' })
)
// Upload pliku
const file = new File(['content'], 'test.pdf', { type: 'application/pdf' })
await user.upload(screen.getByLabelText(/upload/i), file)
})
fireEvent vs userEvent:
// ❌ fireEvent - pojedyncze zdarzenie
fireEvent.click(button)
// Wyzwala tylko: click
// ✅ userEvent - realistyczna sekwencja
await user.click(button)
// Wyzwala: pointerover, pointerenter, mouseover, mouseenter,
// pointermove, mousemove, pointerdown, mousedown, focus,
// pointerup, mouseup, click
Jak testować komponenty z hookami i Context?
Odpowiedź w 30 sekund:
Custom hooks testuj renderHook() z @testing-library/react. Context mockuj przez wrapper w render(). Dla Redux/Zustand stwórz test utilities z providerami.
Odpowiedź w 2 minuty:
Testowanie hooków i komponentów z Context wymaga specjalnych narzędzi RTL - renderHook dla hooków oraz wrapper dla providerów kontekstu.
// CUSTOM HOOKS - renderHook()
import { renderHook, act } from '@testing-library/react'
import { useCounter } from './useCounter'
test('useCounter increments', () => {
const { result } = renderHook(() => useCounter(0))
expect(result.current.count).toBe(0)
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
// Hook z dependency
test('useCounter with different initial value', () => {
const { result, rerender } = renderHook(
({ initial }) => useCounter(initial),
{ initialProps: { initial: 5 } }
)
expect(result.current.count).toBe(5)
// Re-render z nowymi props
rerender({ initial: 10 })
})
// CONTEXT - wrapper
const ThemeContext = React.createContext('light')
function ThemedButton() {
const theme = useContext(ThemeContext)
return <button className={theme}>Click</button>
}
test('uses dark theme from context', () => {
render(
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
)
expect(screen.getByRole('button')).toHaveClass('dark')
})
// Reusable wrapper
const renderWithTheme = (ui: React.ReactNode, theme = 'light') => {
return render(
<ThemeContext.Provider value={theme}>
{ui}
</ThemeContext.Provider>
)
}
test('with wrapper utility', () => {
renderWithTheme(<ThemedButton />, 'dark')
expect(screen.getByRole('button')).toHaveClass('dark')
})
// REDUX - custom render
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
function renderWithRedux(
ui: React.ReactNode,
{ preloadedState = {}, store = configureStore({ reducer: rootReducer, preloadedState }) } = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
store
}
}
test('shows user from Redux store', () => {
renderWithRedux(<UserProfile />, {
preloadedState: { user: { name: 'John' } }
})
expect(screen.getByText('John')).toBeInTheDocument()
})
Cypress - testy E2E
Czym jest Cypress i czym różni się od Selenium?
Odpowiedź w 30 sekund: Cypress to narzędzie do testów E2E działające wewnątrz przeglądarki (nie przez WebDriver jak Selenium). Szybsze, stabilniejsze, z automatycznym czekaniem. Ograniczenia: tylko JavaScript, jedna przeglądarka na test, nie obsługuje wielu tabów.
Odpowiedź w 2 minuty:
Główne różnice między Cypress a Selenium wynikają z odmiennej architektury - Cypress działa wewnątrz przeglądarki, podczas gdy Selenium komunikuje się przez WebDriver.
| Cecha | Cypress | Selenium |
|---|---|---|
| Architektura | Wewnątrz przeglądarki | Przez WebDriver |
| Języki | Tylko JavaScript/TS | Java, Python, C#, Ruby... |
| Szybkość | Bardzo szybki | Wolniejszy |
| Stabilność | Automatyczne retry/wait | Wymaga explicit waits |
| Debugowanie | Time-travel, screenshots | Trudniejsze |
| Przeglądarki | Chrome, Firefox, Edge | Wszystkie |
| Multi-tab | Nie | Tak |
| Cross-domain | Ograniczone | Pełne |
// Cypress - prosty i czytelny
describe('Login Flow', () => {
it('user can login successfully', () => {
cy.visit('/login')
// Automatyczne czekanie na elementy
cy.get('[data-cy=email]').type('user@test.com')
cy.get('[data-cy=password]').type('password123')
cy.get('[data-cy=submit]').click()
// Automatyczne retry asercji
cy.url().should('include', '/dashboard')
cy.contains('Welcome, User').should('be.visible')
})
})
// vs Selenium (Java) - więcej boilerplate
@Test
public void userCanLogin() {
driver.get("http://localhost/login");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement email = wait.until(
ExpectedConditions.presenceOfElementLocated(By.cssSelector("[data-cy=email]"))
);
email.sendKeys("user@test.com");
// ... więcej kodu
}
Jak mockować API w Cypress za pomocą cy.intercept()?
Odpowiedź w 30 sekund:
cy.intercept() przechwytuje żądania sieciowe. Można: mockować odpowiedzi, czekać na żądania (cy.wait('@alias')), modyfikować request/response. Lepsze niż cy.route() (deprecated).
Odpowiedź w 2 minuty:
Funkcja cy.intercept() pozwala na pełną kontrolę nad żądaniami sieciowymi - od prostego mockowania odpowiedzi po zaawansowane scenariusze z opóźnieniami i walidacją requestów.
describe('Products Page', () => {
beforeEach(() => {
// Mock GET /api/products
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: [
{ id: 1, name: 'Product 1', price: 100 },
{ id: 2, name: 'Product 2', price: 200 }
]
}).as('getProducts') // Alias dla wait
})
it('displays products from API', () => {
cy.visit('/products')
// Czekaj na zakończenie requestu
cy.wait('@getProducts')
cy.contains('Product 1').should('be.visible')
cy.contains('Product 2').should('be.visible')
})
it('shows error state', () => {
// Override dla tego testu
cy.intercept('GET', '/api/products', {
statusCode: 500,
body: { error: 'Server error' }
}).as('getProductsError')
cy.visit('/products')
cy.wait('@getProductsError')
cy.contains('Wystąpił błąd').should('be.visible')
})
it('handles slow network', () => {
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: [{ id: 1, name: 'Product 1' }],
delay: 2000 // Symuluj wolną sieć
}).as('slowProducts')
cy.visit('/products')
// Sprawdź loading state
cy.contains('Ładowanie...').should('be.visible')
cy.wait('@slowProducts')
cy.contains('Ładowanie...').should('not.exist')
})
it('verifies request was made correctly', () => {
cy.intercept('POST', '/api/products', (req) => {
// Sprawdź request body
expect(req.body).to.have.property('name', 'New Product')
req.reply({ id: 3, ...req.body })
}).as('createProduct')
cy.visit('/products/new')
cy.get('[data-cy=name]').type('New Product')
cy.get('[data-cy=submit]').click()
cy.wait('@createProduct').its('request.body').should('deep.equal', {
name: 'New Product'
})
})
})
Best practices
Jakie są najlepsze praktyki pisania testów frontend?
Odpowiedź w 30 sekund:
- Testuj zachowanie, nie implementację. 2) Używaj
getByRolejako primary query. 3) Jeden test = jedna rzecz. 4) Unikaj test ID gdzie możliwe. 5) Nie mockuj więcej niż trzeba. 6) Testy powinny być niezależne.
Odpowiedź w 2 minuty:
Zestawienie najczęstszych błędów i najlepszych praktyk w testowaniu frontend - od wyboru odpowiednich queries po strukturę projektów testowych.
// ❌ ZŁYCH PRAKTYK
// 1. Testowanie implementacji
expect(component.state.isOpen).toBe(true) // ❌
expect(screen.getByRole('dialog')).toBeVisible() // ✅
// 2. Test ID jako primary query
screen.getByTestId('submit-btn') // ❌
screen.getByRole('button', { name: /submit/i }) // ✅
// 3. Wiele asercji testujących różne rzeczy
test('form works', () => {
// Testuje walidację, submit, error, success w jednym...
})
// 4. Testy zależne od siebie
let userId // Ustawiane w jednym teście, używane w innym ❌
// ✅ DOBRYCH PRAKTYK
// 1. Arrange-Act-Assert (AAA)
test('adds item to cart', async () => {
// Arrange
render(<ProductPage />)
// Act
await userEvent.click(screen.getByRole('button', { name: /add to cart/i }))
// Assert
expect(screen.getByText('1 item in cart')).toBeInTheDocument()
})
// 2. Testy izolowane
beforeEach(() => {
jest.clearAllMocks()
// Fresh state dla każdego testu
})
// 3. Opisowe nazwy testów
test('shows validation error when email is invalid', () => { ... })
test('disables submit button while form is submitting', () => { ... })
// 4. Test utilities dla powtarzalnego setup
const renderWithProviders = (ui, options) => {
return render(
<QueryClientProvider client={queryClient}>
<ThemeProvider>
{ui}
</ThemeProvider>
</QueryClientProvider>,
options
)
}
// 5. Mockuj na właściwym poziomie
// ❌ Mock fetch globalnie
// ✅ Mock API layer lub użyj MSW
Struktura plików:
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx ← Testy obok komponentu
│ │ └── Button.module.css
├── hooks/
│ ├── useAuth.ts
│ └── useAuth.test.ts
├── utils/
│ ├── formatters.ts
│ └── formatters.test.ts
└── __tests__/ ← Integration tests
└── checkout-flow.test.tsx
Zobacz też
Jeśli przygotowujesz się do rozmowy jako Frontend Developer, sprawdź również:
- Kompletny Przewodnik - Rozmowa Frontend Developer - pełny przewodnik przygotowania do rozmowy frontend
- Najtrudniejsze Pytania React - zaawansowane koncepcje React
- React Hooks - useEffect, useMemo, useCallback - kiedy NIE używać hooków
- Redux Pytania Rekrutacyjne - Redux Toolkit, React-Redux, Thunk
- Najtrudniejsze Pytania JavaScript - zaawansowane koncepcje JS
Powodzenia na rozmowie! Testowanie to obszar, który wyróżnia seniorów od juniorów. Pokaż, że rozumiesz piramidę testów, potrafisz pisać testy odporne na refaktoryzację, i wiesz kiedy użyć unit tests a kiedy E2E.
Chcesz więcej pytań rekrutacyjnych?
To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.
