Fiszki Online Jest (Preview)
Darmowy podgląd 15 z 55 dostępnych pytań
Podstawy Jest
Czym jest Jest i dlaczego jest tak popularny w ekosystemie JavaScript?
Odpowiedź w 30 sekund: Jest to framework testowy stworzony przez Facebooka (obecnie utrzymywany przez OpenJS Foundation), zaprojektowany jako kompletne rozwiązanie "wszystko w jednym" do testowania kodu JavaScript. Popularność zawdzięcza zerowej konfiguracji, wbudowanym asercjom, mockom i pokryciu kodu oraz świetnej integracji z Reactem i innymi nowoczesnymi narzędziami.
Odpowiedź w 2 minuty: Jest powstał w 2014 roku w Facebooku jako odpowiedź na potrzebę testowania ogromnej bazy kodu React. Jego głównym założeniem było zapewnienie narzędzia, które "po prostu działa" bez konieczności składania ekosystemu z wielu bibliotek (jak w przypadku Mocha + Chai + Sinon + Istanbul). Dziś jest standardem de facto w projektach Reactowych, Next.js, a także często wybierany w aplikacjach Node.js oraz bibliotekach uniwersalnych.
Popularność Jest wynika z kilku czynników: prostej konfiguracji (w wielu projektach wystarczy npm install --save-dev jest), wbudowanego runnera, asercji (expect), mocków (jest.fn, jest.mock), pokrycia kodu (--coverage), a także unikalnych funkcji jak snapshot testing. Dodatkowo Jest działa w izolowanych procesach Node.js, co daje deterministyczne i równoległe wykonywanie testów.
Warto używać Jest, gdy zależy nam na szybkim starcie, dobrej dokumentacji i wsparciu społeczności. Typowe pułapki to: domyślne środowisko jsdom (od Jest 27 zmienione na node — łatwo zapomnieć ustawić testEnvironment: 'jsdom' dla testów DOM-owych), problemy z transformacją ESM/TypeScript wymagające babel-jest lub ts-jest, oraz mockowanie modułów ESM (które do niedawna było eksperymentalne).
Przykład kodu:
// suma.js - prosty moduł do testowania
function suma(a, b) {
return a + b;
}
module.exports = { suma };
// suma.test.js - test napisany w Jest
const { suma } = require('./suma');
test('dodaje 2 + 3 i zwraca 5', () => {
// Asercja wbudowana - nie potrzeba Chai ani innej biblioteki
expect(suma(2, 3)).toBe(5);
});
test('obsługuje liczby ujemne', () => {
expect(suma(-1, -1)).toBe(-2);
});
Materiały
↑ Powrót na góręJakie są kluczowe cechy Jest, które wyróżniają go na tle innych frameworków testowych?
Odpowiedź w 30 sekund: Jest wyróżnia się modelem "batteries-included" — w jednym pakiecie dostajemy runner, asercje, mocki, snapshot testing oraz raporty pokrycia kodu. Dodatkowo oferuje równoległe wykonywanie testów w izolowanych procesach, tryb watch oraz świetne komunikaty błędów.
Odpowiedź w 2 minuty:
Najważniejsze cechy Jest to: zerowa konfiguracja dla większości projektów (zwłaszcza Create React App, Next.js), wbudowane asercje przez API expect (toBe, toEqual, toHaveBeenCalled itd.), system mockowania (jest.fn, jest.spyOn, jest.mock) pozwalający na podmianę całych modułów, oraz snapshot testing — porównywanie wyniku komponentu/struktury z zapisanym wcześniej "zdjęciem".
Kolejne istotne cechy to izolacja testów — każdy plik testowy działa we własnym kontekście (workerze), co eliminuje problemy ze współdzielonym stanem. Równoległość sprawia, że duże suity testów działają znacznie szybciej. Tryb watch (jest --watch) inteligentnie uruchamia tylko testy powiązane ze zmienionymi plikami (poprzez analizę grafu zależności git). Code coverage (--coverage) działa out-of-the-box z wykorzystaniem Istanbul/V8.
Warto wspomnieć o timer mocks (jest.useFakeTimers), które pozwalają testować kod oparty o setTimeout/setInterval bez czekania, oraz o module mocking — możliwości podmiany całego modułu zewnętrznego (np. axios) jedną linią kodu. Pułapką bywa to, że tak rozbudowany framework może maskować problemy projektowe — gdy mockujemy "wszystko", nasze testy przestają cokolwiek weryfikować w realnym świecie.
Przykład kodu:
// Przykład pokazujący kilka kluczowych cech Jest naraz
const axios = require('axios');
jest.mock('axios'); // Automatyczne mockowanie modułu
describe('Pobieranie użytkownika', () => {
beforeEach(() => {
// Czyszczenie wywołań mocków przed każdym testem
jest.clearAllMocks();
});
test('pobiera dane i tworzy snapshot', async () => {
// Mockowanie odpowiedzi HTTP
axios.get.mockResolvedValue({ data: { id: 1, name: 'Anna' } });
const response = await axios.get('/api/user/1');
// Asercja - sprawdzenie wywołania mocka
expect(axios.get).toHaveBeenCalledWith('/api/user/1');
// Snapshot - porównanie ze "zdjęciem" zapisanym przy pierwszym uruchomieniu
expect(response.data).toMatchSnapshot();
});
});
Materiały
↑ Powrót na góręJaka jest różnica między Jest a Mocha lub Jasmine?
Odpowiedź w 30 sekund: Mocha to minimalistyczny test runner wymagający dodatkowych bibliotek do asercji (Chai) i mocków (Sinon). Jasmine to framework "wszystko w jednym" — zbliżony filozofią do Jest, ale starszy i pozbawiony nowoczesnych funkcji jak snapshot testing czy równoległe wykonywanie. Jest łączy zalety obu: kompletność Jasmine i nowoczesność Mocha.
Odpowiedź w 2 minuty: Mocha (od 2011) to bardzo elastyczny runner, który celowo nie narzuca asercji ani biblioteki mockującej — pozwala dobrać dowolne narzędzia (Chai, Sinon, Should.js, NYC dla coverage). To podejście daje dużą swobodę, ale wymaga większej konfiguracji i znajomości ekosystemu. Mocha jest popularna w bibliotekach Node.js oraz w starszych projektach.
Jasmine (od 2010, stworzony przez Pivotal Labs) to framework BDD oferujący kompletne rozwiązanie: runner, asercje (expect), mocki (spyOn). Składnia (describe, it, beforeEach) była inspiracją dla Jest. Jest najczęściej używany w starszych projektach Angular (przed migracją na Karma+Jasmine, a obecnie często do Jest).
Jest (od 2014) zbudowano bazując na Jasmine, ale dodano: równoległość, snapshoty, lepsze mocki, tryb watch, izolację modułów oraz wbudowane coverage. W praktyce Jest jest dziś najpopularniejszy w świecie React/Next/TypeScript, podczas gdy Mocha utrzymuje silną pozycję w projektach Node.js i bibliotekach niewykorzystujących Reacta.
Wybór zależy od projektu: Jest dla aplikacji frontendowych i monorepo, Mocha dla bibliotek Node.js wymagających minimalnych zależności, Jasmine — głównie w istniejących projektach.
| Cecha | Jest | Mocha | Jasmine |
|---|---|---|---|
| Runner | tak | tak | tak |
| Wbudowane asercje | tak (expect) |
nie (Chai) | tak (expect) |
| Wbudowane mocki | tak (jest.fn) |
nie (Sinon) | tak (spyOn) |
| Snapshot testing | tak | nie | nie |
| Coverage | tak (wbudowany) | nie (NYC) | nie |
| Równoległość | tak | tylko z mocha-parallel | nie |
| Tryb watch | tak (smart) | tak (--watch) |
nie |
| Konfiguracja | minimalna | wymagana | minimalna |
| Popularność (2024) | bardzo wysoka | wysoka | niska |
Przykład kodu:
// Ten sam test w trzech frameworkach
// === MOCHA + CHAI ===
const { expect } = require('chai');
describe('Suma', () => {
it('dodaje liczby', () => {
expect(2 + 3).to.equal(5); // Chai - inny styl asercji
});
});
// === JASMINE ===
describe('Suma', () => {
it('dodaje liczby', () => {
expect(2 + 3).toBe(5); // Wbudowane expect
});
});
// === JEST ===
describe('Suma', () => {
test('dodaje liczby', () => {
expect(2 + 3).toBe(5); // Praktycznie identyczne jak Jasmine
});
});
Materiały
- Jest vs Mocha vs Jasmine - porównanie
- Mocha - oficjalna dokumentacja
- Jasmine - oficjalna dokumentacja
Jak Jest odnajduje pliki testowe w projekcie (testMatch, testPathIgnorePatterns)?
Odpowiedź w 30 sekund:
Jest domyślnie wyszukuje pliki w katalogach __tests__ oraz pliki kończące się na .test.js, .spec.js (i ich warianty z TypeScript/JSX). Zachowanie można dostosować poprzez opcje testMatch (lista glob patterns) oraz testPathIgnorePatterns (ścieżki do pominięcia, domyślnie node_modules).
Odpowiedź w 2 minuty:
Domyślna konfiguracja testMatch w Jest wygląda tak: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"]. Oznacza to, że Jest znajdzie pliki w katalogach __tests__ (na dowolnym poziomie zagnieżdżenia) oraz wszystkie pliki o rozszerzeniu .test.js, .test.ts, .test.jsx, .test.tsx, .spec.js itd.
testPathIgnorePatterns domyślnie ma wartość ["/node_modules/"], więc Jest nie skanuje zależności. Często dodaje się tu również /dist/, /build/, /.next/ lub inne katalogi build-output, by uniknąć podwójnego uruchamiania testów na skompilowanym kodzie.
Alternatywą dla testMatch jest testRegex (wzajemnie się wykluczają) — wyrażenie regularne dopasowujące ścieżki testów. Generalnie zaleca się testMatch, bo glob jest czytelniejszy.
Warto pamiętać o rootDir (domyślnie katalog z package.json) i roots (lista katalogów, w których Jest szuka testów — domyślnie ["<rootDir>"]). W monorepo często ustawia się roots: ["<rootDir>/packages"] lub używa projects dla wielu konfiguracji.
Pułapki: gdy używamy katalogu __tests__, każdy plik tam trafiający — także helpers.js czy setup.js — zostanie potraktowany jako test (i Jest rzuci błąd "Your test suite must contain at least one test"). Rozwiązaniem jest umieszczanie helperów poza __tests__ albo dodanie ich do testPathIgnorePatterns.
Przykład kodu:
// jest.config.js
module.exports = {
// Gdzie Jest ma szukać testów
roots: ['<rootDir>/src', '<rootDir>/tests'],
// Wzorce dopasowania plików testowych
testMatch: [
'**/__tests__/**/*.[jt]s?(x)', // Katalogi __tests__
'**/?(*.)+(spec|test).[jt]s?(x)' // *.test.js, *.spec.ts itp.
],
// Ścieżki ignorowane (regex, nie glob!)
testPathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/build/',
'/__tests__/helpers/' // Pomijamy helpery w __tests__
],
// Alternatywa - testRegex (zamiast testMatch)
// testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
};
// Aby zobaczyć, które pliki Jest znajdzie, uruchom:
// npx jest --listTests
Materiały
↑ Powrót na góręKonfiguracja i Setup
Jak używać setupFiles oraz setupFilesAfterEach i czym się różnią?
Odpowiedź w 30 sekund:
setupFiles uruchamia się przed załadowaniem frameworka testowego — używany do polyfilli i konfiguracji środowiska. setupFilesAfterEach (właściwa nazwa to setupFilesAfterEach) uruchamia się po załadowaniu frameworka — używany do globalnych matcherów (jest-dom), mocków i hooków beforeEach/afterEach.
Odpowiedź w 2 minuty:
Jest oferuje kilka opcji konfiguracyjnych dla skryptów uruchamianych przed testami, a każda działa na innym etapie cyklu życia. setupFiles to tablica skryptów uruchamianych przed inicjalizacją frameworka testowego w każdym pliku testowym. Na tym etapie nie są jeszcze dostępne globalne API Jest jak expect, beforeEach, jest.mock — używamy go więc dla polyfilli (whatwg-fetch, intersection-observer), ustawiania zmiennych środowiskowych, czy konfiguracji globalnych obiektów.
setupFilesAfterEach (uwaga: właściwa nazwa to setupFilesAfterEach) działa po załadowaniu frameworka. Mamy tu dostęp do całego API Jest — można rejestrować globalne hooki beforeAll/beforeEach/afterEach/afterAll, rozszerzać expect o customowe matchery (np. @testing-library/jest-dom), konfigurować Testing Library (np. configure({ asyncUtilTimeout: 5000 })) oraz mockować moduły globalne.
Dodatkowo Jest oferuje globalSetup i globalTeardown — pliki uruchamiane raz przed/po całej suite testowej (nie per plik). Używane do startu bazy danych, generowania danych testowych, czy uruchomienia serwera mockowego.
| Opcja | Kiedy | Dostęp do API Jest | Typowe użycie |
|---|---|---|---|
globalSetup |
Raz przed wszystkimi testami | Nie | Start DB, init serwera |
setupFiles |
Przed frameworkiem, per plik | Nie | Polyfille, env vars |
setupFilesAfterEach |
Po frameworku, per plik | Tak | jest-dom, globalne mocki |
globalTeardown |
Raz po wszystkich testach | Nie | Cleanup DB, stop serwera |
Przykład kodu:
// jest.config.js
module.exports = {
// Uruchamiane RAZ przed wszystkimi testami
globalSetup: '<rootDir>/jest.global-setup.ts',
globalTeardown: '<rootDir>/jest.global-teardown.ts',
// Uruchamiane PRZED frameworkiem, dla każdego pliku testowego
setupFiles: ['<rootDir>/jest.polyfills.ts'],
// Uruchamiane PO frameworku, dla każdego pliku testowego
setupFilesAfterEach: ['<rootDir>/jest.setup.ts'],
};
// === jest.polyfills.ts - setupFiles ===
// Tu NIE MAMY dostępu do API Jest (expect, beforeEach, jest.mock)
// Tylko polyfille i konfiguracja środowiska
// Polyfill dla fetch w środowisku jsdom
import 'whatwg-fetch';
// Polyfill dla IntersectionObserver
import 'intersection-observer';
// Ustawianie zmiennych środowiskowych
process.env.API_URL = 'http://localhost:3000';
process.env.NODE_ENV = 'test';
// Globalna konfiguracja - np. timezone
process.env.TZ = 'UTC';
// === jest.setup.ts - setupFilesAfterEach ===
// Tu MAMY pełen dostęp do API Jest
import '@testing-library/jest-dom'; // Rozszerza expect o matchery DOM
import { configure } from '@testing-library/react';
import { server } from './mocks/server';
// Konfiguracja Testing Library
configure({ asyncUtilTimeout: 5000 });
// Globalne hooki dla wszystkich testów
beforeAll(() => {
// Start MSW (Mock Service Worker) przed wszystkimi testami
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
// Reset handlerów po każdym teście
server.resetHandlers();
// Czyszczenie mocków
jest.clearAllMocks();
});
afterAll(() => {
// Zatrzymanie MSW po wszystkich testach
server.close();
});
// Mockowanie globalnego obiektu window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
})),
});
// === jest.global-setup.ts - uruchamia się RAZ ===
module.exports = async () => {
// Np. start testowej bazy danych
console.log('Uruchamianie środowiska testowego...');
// await startTestDatabase();
};
Materiały
↑ Powrót na góręMockowanie - jest.fn i jest.mock
Czym różni się manual mock od automock w Jest?
Odpowiedź w 30 sekund:
Automock to globalne ustawienie (automock: true w jest.config) – Jest automatycznie mockuje WSZYSTKIE importowane moduły, generując zamienniki na podstawie ich struktury. Manual mock to ręcznie napisany plik w folderze __mocks__, który definiuje konkretne zachowanie dla wybranego modułu i jest aktywowany przez jest.mock() w teście.
Odpowiedź w 2 minuty:
Automock (automock: true) to mechanizm globalny – Jest analizuje strukturę każdego modułu (funkcje, klasy, obiekty) i tworzy automatyczną wersję mockowaną przy każdym imporcie. Wszystkie funkcje stają się jest.fn() zwracającymi undefined, klasy są opakowane, ale prymitywy i typy zachowują się natywnie. Aby użyć oryginalnego modułu, trzeba jawnie wywołać jest.unmock('./moduł') lub jest.requireActual().
W praktyce automock jest rzadko używany w nowoczesnych projektach – jest zbyt agresywny, prowadzi do testów, gdzie wszystko jest zmockowane "domyślnie", co utrudnia rozumowanie o testach i ich zachowaniu. Domyślnie automock jest wyłączony (false) w Jest i pozostawia kontrolę programiście.
Manual mock to ręcznie napisany plik w folderze __mocks__ obok mockowanego modułu (lub w korzeniu dla node_modules). Plik eksportuje dokładnie to, co oryginał, ale z mockowaną implementacją. Manual mock jest aktywowany przez jest.mock('./moduł') w teście (bez drugiego argumentu). Dla pakietów z node_modules manual mock w korzeniu projektu (<rootDir>/__mocks__/) jest aktywowany automatycznie, ALE dla user modules trzeba zawsze wywołać jest.mock() w teście.
| Cecha | Automock | Manual Mock |
|---|---|---|
| Konfiguracja | Globalna (jest.config.js) |
Per moduł (__mocks__/) |
| Zachowanie | Wszystkie funkcje to jest.fn() → undefined |
Customowa implementacja |
| Aktywacja | Automatyczna dla wszystkich | jest.mock() (lub auto dla node_modules) |
| Kontrola | Mała - generowane przez Jest | Pełna - piszesz sam |
| Przewidywalność | Niska | Wysoka |
| Częstość użycia | Bardzo rzadko | Często |
Wskazówka praktyczna: Trzymaj automock: false (domyślne) i używaj manual mocks dla zależności używanych w wielu testach (np. konfiguracja API klienta, modułów z bazą danych). Dla jednorazowych mocków używaj jest.mock() z factory function w pliku testu – to lepsze niż automock, bo intencja jest jawna.
Przykład kodu:
// === AUTOMOCK - globalna konfiguracja ===
// jest.config.js
module.exports = {
automock: true, // RZADKO używane!
};
// services/payments.js
export const procesujPłatność = (kwota) => {
console.log(`Przetwarzam ${kwota}zł`);
return { status: 'success' };
};
// payment.test.js - z automock = true
import { procesujPłatność } from './services/payments';
test('automock zamienia wszystko w jest.fn()', () => {
// Funkcja jest automatycznie zmockowana - zwraca undefined
expect(procesujPłatność(100)).toBeUndefined();
expect(procesujPłatność).toHaveBeenCalledWith(100);
// Aby użyć oryginału:
jest.unmock('./services/payments');
});
// === MANUAL MOCK - kontrolowane podejście ===
// Struktura:
// services/
// emailService.js
// __mocks__/
// emailService.js <- nasz manual mock
// services/emailService.js (oryginał)
import nodemailer from 'nodemailer';
export const wyślijEmail = async (to, subject, body) => {
const transporter = nodemailer.createTransport({ /* prawdziwa konfiguracja SMTP */ });
return await transporter.sendMail({ to, subject, html: body });
};
// services/__mocks__/emailService.js (manual mock)
export const wyślijEmail = jest.fn(async (to, subject, body) => {
// Symulacja powodzenia bez wysyłania prawdziwego emaila
return {
messageId: 'mock-id-123',
accepted: [to],
rejected: [],
};
});
// W teście aktywujemy manual mock:
import { wyślijEmail } from './services/emailService';
jest.mock('./services/emailService'); // używa __mocks__/emailService.js
test('manual mock daje przewidywalne zachowanie', async () => {
const wynik = await wyślijEmail('test@example.com', 'Witaj', '<p>Cześć</p>');
expect(wynik.messageId).toBe('mock-id-123');
expect(wynik.accepted).toContain('test@example.com');
expect(wyślijEmail).toHaveBeenCalledTimes(1);
});
// === MANUAL MOCK DLA NODE_MODULES ===
// Plik: __mocks__/axios.js (w korzeniu projektu)
const mockAxios = {
get: jest.fn(() => Promise.resolve({ data: { success: true } })),
post: jest.fn(() => Promise.resolve({ data: { id: 42 } })),
create: jest.fn(() => mockAxios), // dla axios.create()
};
export default mockAxios;
// W teście:
import axios from 'axios';
jest.mock('axios'); // automatycznie używa __mocks__/axios.js
test('axios jest zmockowany globalnie', async () => {
const response = await axios.get('/api/dane');
expect(response.data).toEqual({ success: true });
});
Materiały
↑ Powrót na góręHooks i Lifecycle
Jakie są dostępne hooki lifecycle (beforeEach, afterEach, beforeAll, afterAll)?
Odpowiedź w 30 sekund:
Jest udostępnia cztery hooki lifecycle: beforeAll i afterAll (wykonywane raz przed/po wszystkich testach w bloku), oraz beforeEach i afterEach (wykonywane przed/po każdym pojedynczym teście). Służą do przygotowania i sprzątania środowiska testowego, np. inicjalizacji bazy danych, czyszczenia mocków czy resetowania stanu modułów.
Odpowiedź w 2 minuty:
Hooki lifecycle w Jest pozwalają oddzielić logikę przygotowania (setup) i sprzątania (teardown) od samych asercji testowych. Dzięki temu testy stają się czytelniejsze, a kod konfiguracyjny nie jest duplikowany w każdym test().
beforeAll(fn, timeout) uruchamia funkcję raz przed wszystkimi testami w danym bloku describe (lub w pliku, jeśli wywołana na najwyższym poziomie). Idealna do kosztownych operacji, których wynik można współdzielić między testami, np. nawiązanie połączenia z bazą danych, uruchomienie serwera testowego czy załadowanie dużego pliku konfiguracyjnego.
afterAll(fn, timeout) jest jej odpowiednikiem dla sprzątania - wykonuje się raz po zakończeniu wszystkich testów w bloku. Najczęściej używana do zamykania połączeń, zatrzymywania serwerów czy usuwania plików tymczasowych. Pominięcie afterAll może prowadzić do "wiszących" zasobów i nieprawidłowego zakończenia procesu testowego.
beforeEach(fn, timeout) wykonuje się przed każdym testem w bloku. To najczęściej używany hook, ponieważ gwarantuje izolację testów - każdy test startuje z czystym, świeżym stanem. Typowe zastosowania to tworzenie nowej instancji testowanej klasy, resetowanie mocków czy odtwarzanie danych testowych. afterEach(fn, timeout) analogicznie sprząta po każdym teście. Wszystkie hooki przyjmują opcjonalny parametr timeout (domyślnie 5000 ms), który nadpisuje globalny limit czasu dla danego hooka. Hooki mogą zwracać Promise lub używać async/await - Jest poczeka na ich zakończenie przed uruchomieniem testu.
Przykład kodu:
// Przykład wykorzystania wszystkich czterech hooków lifecycle
const { connectDB, disconnectDB, seedDatabase, clearDatabase } = require('./db');
const UserService = require('./UserService');
describe('UserService', () => {
let userService;
// Uruchamiane RAZ przed wszystkimi testami - nawiązujemy połączenie z DB
beforeAll(async () => {
await connectDB('mongodb://localhost:27017/test');
console.log('Połączono z testową bazą danych');
}, 10000); // Zwiększony timeout do 10 sekund
// Uruchamiane PRZED KAŻDYM testem - świeży stan
beforeEach(async () => {
await seedDatabase(); // Wypełnij DB danymi testowymi
userService = new UserService(); // Nowa instancja serwisu
});
// Uruchamiane PO KAŻDYM teście - sprzątanie stanu
afterEach(async () => {
await clearDatabase(); // Wyczyść wszystkie rekordy
jest.clearAllMocks(); // Wyzeruj wszystkie mocki
});
// Uruchamiane RAZ po wszystkich testach - zamknięcie zasobów
afterAll(async () => {
await disconnectDB();
console.log('Rozłączono z bazą danych');
});
test('powinien zwrócić użytkownika po ID', async () => {
const user = await userService.findById('123');
expect(user.name).toBe('Jan Kowalski');
});
test('powinien utworzyć nowego użytkownika', async () => {
const newUser = await userService.create({ name: 'Anna' });
expect(newUser.id).toBeDefined();
});
});
Materiały
↑ Powrót na góręCode Coverage
Czym jest collectCoverageFrom i dlaczego nie wystarczy samo --coverage?
Odpowiedź w 30 sekund:
collectCoverageFrom to tablica wzorców glob określająca, z których plików Jest ma zbierać metryki pokrycia – również z tych, które nie zostały zaimportowane przez żaden test. Bez tej opcji --coverage raportuje tylko pliki dotknięte przez testy, co fałszywie zawyża wynik i ukrywa moduły bez żadnych testów.
Odpowiedź w 2 minuty:
Domyślne zachowanie jest --coverage ma poważną wadę: Jest zbiera pokrycie tylko z plików, które zostały załadowane przez testy. Jeśli stworzysz nowy moduł src/services/payments.js i nie napiszesz dla niego ŻADNEGO testu, to ten plik w ogóle nie pojawi się w raporcie. Może wyglądać, że masz 95% pokrycia, podczas gdy w rzeczywistości całe katalogi pozostają nieprzetestowane.
collectCoverageFrom rozwiązuje ten problem – mówi Jest: „uwzględnij w raporcie WSZYSTKIE pliki pasujące do tych wzorców, nawet jeśli nie ma dla nich testów". Plik bez testów pojawi się w raporcie z pokryciem 0%, co natychmiast obniży statystyki globalne i wymusi pisanie testów dla nowych modułów.
Składnia używa wzorców glob z negacjami (! na początku) do wykluczenia plików, które nie powinny być testowane:
- Pliki konfiguracyjne (
*.config.js,setup.js) - Definicje typów (
*.d.ts, choć dotyczy głównie TypeScript) - Pliki barrel/index eksportujące tylko reeksporty
- Mocki i testy (
__tests__/,*.test.js,*.mock.js) - Storybook (
*.stories.js) - Generowany kod (np. GraphQL codegen, pliki z
dist/)
W połączeniu z coverageThreshold daje to potężny mechanizm: definiujesz, co ma być pokryte (collectCoverageFrom), ile musi być pokryte (coverageThreshold), a CI pilnuje obu warunków. To standard dla projektów produkcyjnych, w których nie da się zaufać samemu „mamy 80% pokrycia".
Przykład kodu:
// jest.config.js
module.exports = {
// Wlacz zbieranie pokrycia
collectCoverage: true,
// Z czego zbierac pokrycie - kluczowa opcja!
collectCoverageFrom: [
// Wszystkie pliki JS/TS w src
'src/**/*.{js,jsx,ts,tsx}',
// Wykluczenia - pliki, ktore NIE wchodza do statystyk
'!src/**/*.d.ts', // Definicje typow TypeScript
'!src/**/*.test.{js,ts}', // Same testy
'!src/**/*.spec.{js,ts}', // Spec files
'!src/**/__tests__/**', // Foldery testowe
'!src/**/__mocks__/**', // Foldery z mockami
'!src/**/*.stories.{js,tsx}', // Storybook stories
'!src/index.js', // Glowny barrel file
'!src/types/**', // Tylko deklaracje typow
'!src/**/*.config.js', // Pliki konfiguracyjne
'!**/node_modules/**',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'html', 'lcov'],
// Polaczenie z progami daje pelna kontrole
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
// PRZED dodaniem collectCoverageFrom:
// Stmts | Branch | Funcs | Lines
// 95% | 90% | 92% | 95% <- "Super wyniki!"
// Ale: src/services/newFeature.js w ogole nie jest w raporcie
// PO dodaniu collectCoverageFrom:
// Stmts | Branch | Funcs | Lines
// 62% | 58% | 65% | 62% <- Realny obraz
// Widac: src/services/newFeature.js - 0% pokrycia
Materiały
↑ Powrót na góręBest Practices i Wzorce
Jak pisać testy odporne na refaktoring (test behavior, not implementation)?
Odpowiedź w 30 sekund: Test odporny na refaktoring sprawdza zachowanie widoczne z perspektywy użytkownika lub konsumenta API, a nie sposób w jaki jest to wewnętrznie zrealizowane. Filozofia Kenta C. Doddsa "test like a user" mówi: jeśli test nie pęka po zmianie wewnętrznej implementacji (która nie zmieniła obserwowalnego zachowania), to znaczy że dobrze go napisałeś.
Odpowiedź w 2 minuty: Kent C. Dodds w słynnym poście "Testing Implementation Details" zdefiniował dwa rodzaje false outputs testów: false negatives (test pęka mimo że aplikacja działa — efekt testowania implementacji) i false positives (test przechodzi mimo że aplikacja jest zepsuta — efekt over-mockingu i pomijania realnej integracji). Oba są zabójcze dla zaufania do test suite.
Praktyczne zasady "testowania zachowania":
Pytaj: co użytkownik (lub klient API) zobaczy/dostanie? Test powinien sprawdzać tylko to. Nie sprawdzaj że wywołano konkretny
useState, że pole nazywa się_internalCounter, ani że pętla używareduce. Sprawdź że na ekranie pojawia się "5 pozycji w koszyku".Używaj zapytań accessibility-first (Testing Library):
getByRole,getByLabelText,getByText. Te zapytania odzwierciedlają to jak realny użytkownik (i czytnik ekranu) postrzega UI. Po zmianie klasy CSS test się nie wywróci.Mockuj na granicy, nie wewnątrz. Granice systemu to: HTTP (
fetch,axios), baza danych, zegar (Date.now(), timery), randomizacja, file system. Wewnętrzne funkcje i moduły zostaw prawdziwe — wtedy refactoring (przeniesienie funkcji, zmiana nazwy, ekstrakcja) nie psuje testów.Public API zamiast internals. Jeśli testujesz moduł, testuj jego eksportowane funkcje. Nie sięgaj po
module.__internal__.helper(). Jeśli helper jest zbyt złożony by testować pośrednio, prawdopodobnie powinien być osobnym modułem z własnym public API.Test "rozumie cel", nie "sposób". Zamiast
expect(arr.length).toBe(3), napiszexpect(screen.getByText('3 produkty w koszyku')).toBeInTheDocument(). Ten drugi przetrwa zmianę z tablicy na Set.Integration > Unit (dla feature'ów). Testy integracyjne (kilka komponentów razem, z fake'owym HTTP przez MSW) są naturalnie odporne na refaktoring wewnętrzny — bo testują efekt końcowy, nie konkretną dekompozycję.
Przykład kodu:
// Komponent
function ListaZadan({ zadania, onUsun }) {
const [filtr, setFiltr] = useState('wszystkie');
const widoczne = zadania.filter(z =>
filtr === 'wszystkie' || (filtr === 'aktywne' ? !z.zrobione : z.zrobione)
);
return (
<div>
<select value={filtr} onChange={e => setFiltr(e.target.value)}
aria-label="Filtr zadan">
<option value="wszystkie">Wszystkie</option>
<option value="aktywne">Aktywne</option>
<option value="zakonczone">Zakonczone</option>
</select>
<ul>
{widoczne.map(z => (
<li key={z.id}>
{z.tekst}
<button onClick={() => onUsun(z.id)}>Usun</button>
</li>
))}
</ul>
</div>
);
}
// ZLY: testuje implementacje - pekn ie po refactorze useState -> useReducer
it('zly - testuje implementacje', () => {
const { container } = render(<ListaZadan zadania={[]} onUsun={jest.fn()} />);
// Sprawdzamy konkretne klasy/strukture DOM
expect(container.querySelector('select.filtr')).toBeInTheDocument();
expect(container.querySelectorAll('ul > li')).toHaveLength(0);
});
// DOBRY: testuje zachowanie - przetrwa refactoring wewnetrzny
it('powinno filtrowac zadania po wybraniu opcji aktywne', async () => {
// Arrange
const uzytkownik = userEvent.setup();
const zadania = [
{ id: 1, tekst: 'Kup mleko', zrobione: false },
{ id: 2, tekst: 'Wyniesc smieci', zrobione: true },
];
render(<ListaZadan zadania={zadania} onUsun={jest.fn()} />);
// Act - to co zrobilby uzytkownik
await uzytkownik.selectOptions(
screen.getByRole('combobox', { name: /filtr zadan/i }),
'aktywne'
);
// Assert - to co zobaczy uzytkownik
expect(screen.getByText('Kup mleko')).toBeInTheDocument();
expect(screen.queryByText('Wyniesc smieci')).not.toBeInTheDocument();
});
it('powinno usunac zadanie po klikniciu przycisku', async () => {
// Arrange
const uzytkownik = userEvent.setup();
const onUsun = jest.fn();
const zadania = [{ id: 1, tekst: 'Kup mleko', zrobione: false }];
render(<ListaZadan zadania={zadania} onUsun={onUsun} />);
// Act
await uzytkownik.click(screen.getByRole('button', { name: /usun/i }));
// Assert - sprawdzamy kontrakt z konsumentem (props)
expect(onUsun).toHaveBeenCalledWith(1);
});
Materiały
- Kent C. Dodds - Testing Implementation Details
- Kent C. Dodds - Avoid the Test User
- Testing Library - Guiding Principles
Asercje i Matchery
Jakie są najczęściej używane matchery w Jest (toBe, toEqual, toContain, toMatch)?
Odpowiedź w 30 sekund:
Jest oferuje bogaty zestaw matcherów do weryfikacji wartości. Najczęściej używane to toBe() dla porównań prymitywów przez referencję, toEqual() dla głębokiego porównania obiektów i tablic, toContain() do sprawdzania zawartości tablic lub ciągów oraz toMatch() do dopasowania ciągów do wyrażenia regularnego lub podciągu.
Odpowiedź w 2 minuty:
Matchery (ang. matchers) to metody dołączone do obiektu zwracanego przez expect(), które pozwalają opisać oczekiwany wynik testu. Każdy matcher wykonuje konkretny rodzaj porównania, a w razie niezgodności generuje czytelny komunikat o błędzie. Dzięki temu nie musimy pisać własnych warunków if ani rzucać błędów ręcznie — Jest robi to za nas.
Najpopularniejsze matchery to: toBe() (porównanie przez Object.is, idealne dla liczb, stringów, booleanów), toEqual() (rekurencyjne porównanie struktury — używane do obiektów i tablic), toContain() (sprawdza czy element jest w tablicy lub czy podciąg występuje w stringu), toMatch() (porównuje string z regexem lub innym stringiem), toBeTruthy() / toBeFalsy() (sprawdzają wartość logiczną), toBeNull(), toBeUndefined(), toBeDefined() (sprawdzają wartości specjalne), toBeGreaterThan() / toBeLessThan() (porównania liczbowe) oraz toHaveLength() (długość tablicy/stringa).
Każdy matcher można zanegować przez .not, np. expect(value).not.toBe(5). Dla operacji asynchronicznych dostępne są dodatkowe modyfikatory .resolves i .rejects. Dobrym zwyczajem jest wybieranie najbardziej specyficznego matchera dla danej sytuacji — daje to lepsze komunikaty błędów i czytelniejsze testy.
Przykład kodu:
// toBe - porównanie przez referencję (Object.is)
test('toBe dla wartości prymitywnych', () => {
expect(2 + 2).toBe(4);
expect('hello').toBe('hello');
expect(true).toBe(true);
});
// toEqual - głębokie porównanie struktury
test('toEqual dla obiektów i tablic', () => {
const uzytkownik = { imie: 'Anna', wiek: 30 };
expect(uzytkownik).toEqual({ imie: 'Anna', wiek: 30 });
const liczby = [1, 2, 3];
expect(liczby).toEqual([1, 2, 3]);
});
// toContain - sprawdzenie zawartości
test('toContain dla tablic i stringów', () => {
const owoce = ['jablko', 'banan', 'gruszka'];
expect(owoce).toContain('banan');
const tekst = 'Witaj w Jest';
expect(tekst).toContain('Jest');
});
// toMatch - dopasowanie do regexa lub podciągu
test('toMatch dla stringów', () => {
expect('team').not.toMatch(/I/);
expect('Christoph').toMatch(/stop/);
expect('numer-123').toMatch(/\d+/);
});
// Negacja przez .not
test('uzycie .not', () => {
expect(5).not.toBe(10);
expect([1, 2, 3]).not.toContain(4);
});
Materiały
↑ Powrót na góręSpies i Reset Mocków
Czym jest spy i kiedy używać jest.spyOn() zamiast jest.fn()?
Odpowiedź w 30 sekund:
Spy to specjalny rodzaj mocka, który "szpieguje" istniejącą metodę obiektu — śledzi jej wywołania, ale domyślnie nadal wykonuje oryginalną implementację. Używaj jest.spyOn(), gdy chcesz monitorować lub tymczasowo podmienić istniejącą metodę bez tracenia jej oryginalnego zachowania, a jest.fn() — gdy tworzysz całkowicie nową, samodzielną funkcję mock.
Odpowiedź w 2 minuty:
jest.fn() tworzy zupełnie nową funkcję mock, która nie jest powiązana z żadnym istniejącym kodem. Świetnie nadaje się do tworzenia callbacków, fałszywych zależności wstrzykiwanych do testowanego kodu lub mockowania funkcji przekazywanych jako argumenty. Mock z jest.fn() domyślnie zwraca undefined i nie ma żadnej implementacji — wszystko musisz dostarczyć samodzielnie przez mockReturnValue, mockImplementation itp.
jest.spyOn(obiekt, 'metoda') natomiast zastępuje istniejącą metodę obiektu funkcją mock, ale zachowuje oryginalną implementację jako domyślne zachowanie. Dzięki temu możesz weryfikować wywołania (toHaveBeenCalledWith), licząc na to, że reszta logiki nadal działa. Spy można w każdej chwili przywrócić do oryginału za pomocą mockRestore() — czego jest.fn() nie potrafi, bo niczego nie zastępuje.
W praktyce spy używamy do testowania metod modułów (np. console.log, Date.now, metod serwisów), API wbudowanych przeglądarki lub statycznych metod klas. Jest to nieoceniona technika, gdy nie chcemy całkowicie podmieniać modułu (jest.mock), a jedynie obserwować pojedynczą metodę i potencjalnie zmienić jej zachowanie tylko w jednym teście.
Przykład kodu:
// uzytkownikService.js
export const uzytkownikService = {
pobierzDane(id) {
return { id, imie: 'Anna' };
},
zapisz(uzytkownik) {
console.log('Zapisuję:', uzytkownik);
return true;
},
};
// uzytkownikService.test.js
import { uzytkownikService } from './uzytkownikService';
describe('uzytkownikService', () => {
// jest.fn() - tworzymy NOWY mock (np. callback)
test('callback po zapisaniu', () => {
const callback = jest.fn();
[1, 2, 3].forEach(callback);
expect(callback).toHaveBeenCalledTimes(3);
expect(callback).toHaveBeenCalledWith(1, 0, [1, 2, 3]);
});
// jest.spyOn() - szpiegujemy ISTNIEJĄCĄ metodę
test('szpiegowanie wywołania bez zmiany implementacji', () => {
const spy = jest.spyOn(uzytkownikService, 'pobierzDane');
const wynik = uzytkownikService.pobierzDane(42);
expect(spy).toHaveBeenCalledWith(42);
expect(wynik).toEqual({ id: 42, imie: 'Anna' }); // oryginał DZIAŁA
spy.mockRestore(); // przywracamy oryginał
});
// jest.spyOn() z podmianą implementacji
test('tymczasowa podmiana zachowania', () => {
const spy = jest
.spyOn(uzytkownikService, 'pobierzDane')
.mockReturnValue({ id: 0, imie: 'Mock' });
expect(uzytkownikService.pobierzDane(1)).toEqual({ id: 0, imie: 'Mock' });
spy.mockRestore();
expect(uzytkownikService.pobierzDane(1)).toEqual({ id: 1, imie: 'Anna' });
});
// Szpiegowanie globalnego API
test('wyciszenie console.log w teście', () => {
const spyLog = jest.spyOn(console, 'log').mockImplementation(() => {});
uzytkownikService.zapisz({ id: 1 });
expect(spyLog).toHaveBeenCalledWith('Zapisuję:', { id: 1 });
spyLog.mockRestore();
});
});
Materiały
↑ Powrót na góręTesty Asynchroniczne
Jak testować kod asynchroniczny używając async/await i Promises?
Odpowiedź w 30 sekund:
Jest obsługuje testy asynchroniczne na trzy sposoby: zwracając Promise z funkcji testowej, używając async/await lub przekazując callback done. Najczystszym i najbardziej czytelnym podejściem jest async/await — Jest automatycznie czeka na zakończenie obietnicy przed zakończeniem testu.
Odpowiedź w 2 minuty: Domyślnie Jest traktuje test jako zakończony, gdy funkcja testowa zwróci wartość. Jeśli test wykonuje operacje asynchroniczne, ale nie zostaną one odpowiednio zasygnalizowane, Jest może uznać test za zaliczony, zanim asercje zostaną faktycznie wykonane. Dlatego ważne jest poprawne sygnalizowanie asynchroniczności.
Pierwsze podejście — zwracanie Promise: jeśli funkcja testowa zwraca Promise, Jest poczeka na jej rozwiązanie. Promise odrzucony spowoduje niepowodzenie testu. Drugie i najczęściej stosowane — async/await: oznaczamy funkcję testową jako async i używamy await do oczekiwania na obietnice. Kod jest wtedy liniowy i czytelny, a błędy są obsługiwane jak w kodzie synchronicznym.
Ważne pułapki: zawsze ustawiaj expect.assertions(N) w testach, w których sprawdzasz odrzucenie obietnicy w bloku catch — bez tego test może przejść, mimo że żadna asercja nie została wykonana. Pamiętaj też o ustawieniu odpowiedniego timeoutu (jest.setTimeout()) dla wolnych operacji — domyślny to 5 sekund.
Przykład kodu:
// Funkcja zwracająca Promise
const pobierzUzytkownika = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) resolve({ id, imie: 'Anna' });
else reject(new Error('Nieprawidłowe ID'));
}, 100);
});
};
// Podejście 1: zwracanie Promise
test('pobiera użytkownika - Promise', () => {
// WAŻNE: musimy zwrócić Promise, inaczej Jest nie zaczeka
return pobierzUzytkownika(1).then((dane) => {
expect(dane.imie).toBe('Anna');
});
});
// Podejście 2: async/await (zalecane)
test('pobiera użytkownika - async/await', async () => {
const dane = await pobierzUzytkownika(1);
expect(dane.imie).toBe('Anna');
});
// Testowanie odrzucenia obietnicy
test('rzuca błąd dla złego ID', async () => {
// expect.assertions zapewnia, że asercja w catch się wykonała
expect.assertions(1);
try {
await pobierzUzytkownika(-1);
} catch (blad) {
expect(blad.message).toBe('Nieprawidłowe ID');
}
});
Materiały
↑ Powrót na góręSnapshot Testing
Czym jest snapshot testing w Jest i jak działa?
Odpowiedź w 30 sekund:
Snapshot testing to technika testowania, w której Jest zapisuje serializowaną reprezentację wyniku (np. wyrenderowanego komponentu UI lub obiektu) do pliku .snap przy pierwszym uruchomieniu testu. Przy kolejnych uruchomieniach Jest porównuje aktualny wynik z zapisanym snapshotem - jeśli się różnią, test nie przechodzi. To szybki sposób na wykrycie nieoczekiwanych zmian w danych wyjściowych.
Odpowiedź w 2 minuty:
Snapshot testing działa na zasadzie "zrób zdjęcie i porównaj". Gdy wywołasz expect(value).toMatchSnapshot() po raz pierwszy, Jest serializuje wartość (używając wbudowanych serializatorów lub własnych) i zapisuje ją do pliku obok testu, w katalogu __snapshots__. Plik ma rozszerzenie .snap i jest zwykłym tekstem, który należy commitować do repozytorium razem z kodem testów.
Przy następnych uruchomieniach testu Jest ponownie serializuje wartość i porównuje ją znakowo z zapisanym snapshotem. Jeśli są identyczne - test przechodzi. Jeśli się różnią - test zawodzi, a Jest pokazuje diff pokazujący dokładnie co się zmieniło. To bardzo szybki sposób testowania złożonych struktur (drzewo DOM, duże obiekty, odpowiedzi API), bo nie musisz pisać wielu asercji na każde pole z osobna.
Snapshoty są szczególnie popularne w testach komponentów React (z react-test-renderer lub Testing Library), gdzie pozwalają wykryć nieoczekiwane zmiany w renderowanym HTML. Pamiętaj jednak, że snapshot testing nie zastępuje testów intencjonalnych - sprawdza tylko, że wynik nie zmienił się od ostatniego zatwierdzenia, ale nie weryfikuje czy jest poprawny. Dlatego pierwszy snapshot zawsze trzeba ręcznie zweryfikować w code review.
Przykład kodu:
// Test komponentu React
import renderer from 'react-test-renderer';
import Button from './Button';
test('renderuje przycisk z etykietą', () => {
const tree = renderer
.create(<Button label="Kliknij mnie" variant="primary" />)
.toJSON();
expect(tree).toMatchSnapshot();
});
// Test obiektu danych
test('formatuje użytkownika', () => {
const user = formatUser({ firstName: 'Anna', lastName: 'Kowalska' });
expect(user).toMatchSnapshot();
});
// Wygenerowany plik: __snapshots__/Button.test.js.snap
/*
exports[`renderuje przycisk z etykietą 1`] = `
<button
className="btn btn--primary"
onClick={[Function]}
>
Kliknij mnie
</button>
`;
exports[`formatuje użytkownika 1`] = `
Object {
"displayName": "Anna Kowalska",
"initials": "AK",
}
`;
*/
// Możesz też używać matcherów wewnątrz snapshotu (Property Matchers)
test('tworzy użytkownika z dynamicznym ID', () => {
const user = createUser({ name: 'Jan' });
expect(user).toMatchSnapshot({
id: expect.any(String), // ignoruj wartość, sprawdź typ
createdAt: expect.any(Date),
});
});
Materiały
↑ Powrót na góręTesty Parametryzowane i Strukturyzacja
Jak używać test.each() i describe.each() do testów parametryzowanych?
Odpowiedź w 30 sekund:
test.each() i describe.each() pozwalają uruchomić ten sam test lub blok testów wielokrotnie z różnymi zestawami danych, eliminując powtarzający się kod. Dostępne są dwie składnie: tablicowa (array of arrays/objects) oraz tagged template literal, która tworzy czytelną tabelę w stylu Markdown.
Odpowiedź w 2 minuty:
Testy parametryzowane są niezastąpione, gdy chcemy zweryfikować zachowanie funkcji dla wielu różnych wejść bez kopiowania logiki testu. Zamiast pisać dziesięć osobnych testów it('should ...'), definiujemy jeden szablon testu i tablicę danych. Jest wygeneruje osobny test case dla każdego wiersza danych, dzięki czemu każda kombinacja ma własną nazwę i niezależny status pass/fail w raporcie.
Składnia tablicowa (array) jest prostsza i lepiej sprawdza się przy małej liczbie parametrów. Każdy element tablicy to z kolei tablica argumentów, które trafiają do funkcji testowej. W nazwie testu używamy printf-style placeholders (%s, %i, %p, %# dla indeksu, %o dla obiektów), aby dynamicznie podstawiać wartości.
Składnia tagged template literal ( test.each ``) jest czytelniejsza, gdy mamy wiele kolumn z nazwanymi parametrami. Pierwsza linia to nagłówki kolumn rozdzielone |, kolejne to wiersze danych. W nazwie testu odwołujemy się do parametrów przez $nazwa, co dodatkowo dokumentuje intencje testu. describe.each() działa analogicznie, ale parametryzuje cały blok describe, co przydaje się przy testowaniu wielu metod tej samej klasy lub wariantów konfiguracji.
Warto pamiętać, że przy obiektach jako parametrach lepiej używać destrukturyzacji w callbacku oraz $nazwa w nazwie testu (zamiast %o), bo wynik jest dużo czytelniejszy. Można też przekazać funkcję jako trzeci argument, aby dynamicznie generować nazwę testu na podstawie danych.
Przykład kodu:
// Składnia tablicowa - prosta, gdy parametrów jest mało
describe('add()', () => {
test.each([
[1, 1, 2],
[2, 3, 5],
[-1, 1, 0],
[0, 0, 0],
])('add(%i, %i) zwraca %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
// Z obiektami i indeksem (%#)
test.each([
{ a: 1, b: 1, expected: 2 },
{ a: 5, b: 5, expected: 10 },
])('test #%# - suma $a + $b = $expected', ({ a, b, expected }) => {
expect(add(a, b)).toBe(expected);
});
});
// Składnia tagged template literal - czytelna tabela
describe('validateEmail()', () => {
test.each`
email | expected
${'user@example.com'} | ${true}
${'invalid'} | ${false}
${''} | ${false}
${'a@b.c'} | ${true}
${null} | ${false}
`('walidacja "$email" zwraca $expected', ({ email, expected }) => {
expect(validateEmail(email)).toBe(expected);
});
});
// describe.each() - parametryzacja całego bloku
describe.each([
{ role: 'admin', canDelete: true, canEdit: true },
{ role: 'editor', canDelete: false, canEdit: true },
{ role: 'viewer', canDelete: false, canEdit: false },
])('Uprawnienia dla roli: $role', ({ role, canDelete, canEdit }) => {
test(`może usuwać: ${canDelete}`, () => {
expect(hasPermission(role, 'delete')).toBe(canDelete);
});
test(`może edytować: ${canEdit}`, () => {
expect(hasPermission(role, 'edit')).toBe(canEdit);
});
});
Materiały
↑ Powrót na góręWydajność i Debugowanie
Jak Jest uruchamia testy równolegle i jak kontrolować workerów (--maxWorkers)?
Odpowiedź w 30 sekund:
Jest domyślnie uruchamia testy równolegle, tworząc pulę procesów workerów (po jednym na rdzeń CPU minus 1), gdzie każdy plik testowy działa w izolowanym procesie potomnym. Liczbą workerów sterujesz flagą --maxWorkers, która przyjmuje liczbę (--maxWorkers=4), procent (--maxWorkers=50%) lub specjalną wartość --runInBand, która wyłącza równoległość i uruchamia wszystko w jednym procesie.
Odpowiedź w 2 minuty:
Jest osiąga równoległość poprzez moduł jest-worker, który tworzy pulę procesów Node.js (child_process.fork) i rozdziela między nie pliki testowe. Każdy worker dostaje pełną, niezależną instancję module registry, co oznacza, że testy nie mogą wpływać na siebie nawzajem przez globalny stan, ale też że każdy worker płaci koszt inicjalizacji (ładowanie modułów, parsowanie konfiguracji, ustawianie jsdom). Pliki testowe wewnątrz jednego workera wykonują się sekwencyjnie, ale różne pliki na różnych workerach – równolegle.
Domyślnie Jest ustawia maxWorkers = liczbaRdzeni - 1 w lokalnym uruchomieniu i maxWorkers = 2 w CI (zmienna CI=true). Flaga --maxWorkers=50% jest szczególnie użyteczna, bo skaluje się z maszyną – świetne dla zespołów z różnym hardware. Wartość liczbowa (--maxWorkers=4) daje deterministyczne zachowanie, przydatne w CI z ograniczonymi zasobami. --runInBand (alias -i) całkowicie wyłącza tworzenie workerów i uruchamia testy w głównym procesie – kluczowe przy debugowaniu (debugger podpina się tylko do jednego procesu) oraz w środowiskach z bardzo małą ilością RAM, gdzie koszt wielu workerów przewyższa zysk z równoległości.
W praktyce sweet spot to często --maxWorkers=50% lokalnie i --maxWorkers=2 lub --maxWorkers=4 w CI. Zbyt wielu workerów powoduje thrashing (CPU spędza czas na context-switch zamiast wykonywaniu testów) i wyczerpanie pamięci, bo każdy worker to osobny proces Node z własną stertą V8.
Przykład kodu:
// package.json - skrypty dla różnych środowisk
{
"scripts": {
// Lokalnie: 50% rdzeni, dobry balans
"test": "jest --maxWorkers=50%",
// CI: stała liczba, deterministyczne zużycie zasobów
"test:ci": "jest --maxWorkers=2 --ci",
// Debugowanie: jeden proces, brak równoległości
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
// Mało pamięci RAM: ograniczamy do 2 workerów
"test:lowmem": "jest --maxWorkers=2"
}
}
graph TD
A[jest CLI] --> B[Main Process<br/>jest-cli]
B --> C{maxWorkers?}
C -->|--runInBand| D[Sekwencyjnie<br/>w main process]
C -->|--maxWorkers=4| E[jest-worker pool]
E --> W1[Worker 1<br/>child_process]
E --> W2[Worker 2<br/>child_process]
E --> W3[Worker 3<br/>child_process]
E --> W4[Worker 4<br/>child_process]
W1 --> T1[test-a.spec.js<br/>test-b.spec.js]
W2 --> T2[test-c.spec.js<br/>test-d.spec.js]
W3 --> T3[test-e.spec.js]
W4 --> T4[test-f.spec.js<br/>test-g.spec.js]
T1 --> R[Aggregated Results]
T2 --> R
T3 --> R
T4 --> R
Materiały
↑ Powrót na górę