Frontend Testing - Jest, React Testing Library, Cypress [2026]

Sławomir Plamowski 18 min czytania
cypress e2e frontend jest pytania-rekrutacyjne react-testing-library testing unit-testing

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

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:

  1. Testuj zachowanie, nie implementację. 2) Używaj getByRole jako 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ż:


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.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.