Unit testing w JavaScript stanowi fundament nowoczesnego podejścia do wytwarzania oprogramowania, pozwalając deweloperom na wczesne wykrywanie błędów, poprawę jakości kodu i budowanie pewności co do poprawności implementacji. Jest, inspirowany Jasmine, oferuje wszechstronne możliwości testowania kodu synchronicznego i asynchronicznego, z wbudowaną obsługą mockowania, testów snapshotowych oraz raportowania pokrycia kodu. W tym opracowaniu omówiono, czym są testy jednostkowe, jak działają, dlaczego są ważne dla procesu tworzenia oprogramowania oraz jak praktycznie wdrożyć je w projektach JavaScript przy użyciu Jest i najlepszych praktyk branżowych.
Fundamenty testowania jednostkowego w JavaScript
Czym są testy jednostkowe i ich znaczenie
Testy jednostkowe (unit tests) stanowią pierwszą linię obrony w strategii zapewniania jakości oprogramowania. Są to testy, które skupiają się na pojedynczych, izolowanych jednostkach kodu – najczęściej funkcjach lub metodach – w celu sprawdzenia, czy działają one zgodnie z oczekiwaniami dla różnych zestawów danych wejściowych. Jednostka testowana powinna być wystarczająco mała, aby mogła być badana w izolacji od pozostałych części systemu, co umożliwia precyzyjne zidentyfikowanie źródła problemu w przypadku błędu.
Znaczenie testów jednostkowych trudno przecenić: działają szybko, stanowią żywą dokumentację i ułatwiają bezpieczną refaktoryzację. W skrócie, ich kluczowe zalety to:
- szybkość – dobrze napisana test suite powinna uruchamiać się w sekundę lub mniej, umożliwiając częste uruchamianie bez opóźnień;
- żywa dokumentacja – czytelny test jasno komunikuje zamierzone zachowanie danego fragmentu kodu;
- bezpieczna refaktoryzacja – pokrycie testami pozwala zmieniać implementację bez ryzyka nieświadomego zepsucia funkcji.
Rodzaje testów w piramidzie testowania
Mimo że niniejszy tekst skupia się na testach jednostkowych, warto rozumieć, że stanowią one bazę hierarchii testów znanej jako „piramida testowania”. Każdy poziom ma inny cel, czas wykonania i poziom pewności, który zapewnia. Oto jej główne warstwy:
- testy jednostkowe – szybkie, niezawodne i wykonywane w izolacji od innych komponentów;
- testy integracyjne – sprawdzają współpracę między wieloma modułami czy serwisami;
- testy end-to-end (E2E) – weryfikują całą aplikację z perspektywy użytkownika, symulując rzeczywiste scenariusze.
Testy jednostkowe są fundamentem, bo są najszybsze, najtańsze w utrzymaniu i dają najszybszą informację zwrotną podczas programowania. Odpowiednio zaprojektowana strategia testowania opiera się na dużej liczbie testów jednostkowych, mniejszej liczbie testów integracyjnych i najmniejszej liczbie testów E2E.
Test-driven development jako metodologia
Cykl red–green–refactor
Test-driven development (TDD) to metodologia programowania, w której testy są pisane przed kodem implementacyjnym, a nie po nim. Proces TDD opiera się na cyklu zwanym red–green–refactor, który składa się z trzech etapów. Na etapie red deweloper pisze test, który nie przechodzi, ponieważ funkcjonalność nie została jeszcze zaimplementowana. Na etapie green deweloper pisze minimalny kod niezbędny do tego, aby test przeszedł. Na etapie refactor deweloper czyści kod, utrzymując testy zielone, aby poprawić jego jakość bez zmiany zachowania.
TDD zmienia sposób myślenia o kodzie: najpierw definiujemy oczekiwane zachowanie w teście, a dopiero potem implementujemy minimalną logikę potrzebną do jego spełnienia. Prowadzi to do kodu z natury testowalnego, który łatwo izolować i sprawdzać.
Zalety test-driven development
TDD niesie wiele korzyści dla procesu tworzenia oprogramowania. Najważniejsze z nich to:
- mniej błędów na produkcji – kod jest testowany od samego początku;
- łatwiejsze debugowanie – niepowodzenie testu natychmiast wskazuje obszar problemu;
- lepszy projekt kodu – powstają małe, dedykowane funkcje i moduły;
- bezpieczna refaktoryzacja – pokrycie testami chroni istniejące zachowania;
- lepsza komunikacja w zespole – testy są żywą specyfikacją oczekiwanego zachowania.
Wprowadzenie do Jest
Historia i pozycja Jest na rynku
Jest to framework do testowania JavaScript, rozwijany i utrzymywany przez Meta (dawniej Facebook). Framework wywodzi się z idei Jasmine, ale został znacząco rozbudowany. Jest zyskał ogromną popularność, szczególnie wśród deweloperów Reacta, dzięki świetnej integracji z tym ekosystemem i domyślnej konfiguracji w create-react-app.
Pozycja Jest na rynku jest bardzo silna – to de facto standard testowania w społeczności JavaScript i TypeScript. Jest oferuje bogatą funkcjonalność „out of the box”, co oznacza, że większość projektów może zacząć testowanie bez dodatkowej konfiguracji. Filozofia Jest brzmi:
działać świetnie domyślnie
Gdy potrzeba większej mocy konfiguracji – ma być dostępna.
Porównanie z innymi narzędziami
W ekosystemie testowania JavaScript istnieją alternatywy dla Jest, takie jak Mocha z Chai, Vitest czy Jasmine. Poniższa tabela syntetycznie porównuje kluczowe cechy tych rozwiązań:
| Narzędzie | Asercje | Mocki wbudowane | Snapshoty | Raport coverage | Konfiguracja domyślna | Wydajność |
|---|---|---|---|---|---|---|
| Jest | Tak | Tak | Tak | Tak | Bardzo dobra | Dobra |
| Mocha + Chai | Tak (Chai) | Nie (Sinon/dodatki) | Nie (dodatki) | Nie (c8/nyc) | Minimalna | Dobra |
| Vitest | Tak | Tak (vi) | Tak | Tak | Dobra | Bardzo dobra |
| Jasmine | Tak | Tak (spies) | Nie | Nie (c8/nyc) | Umiarkowana | Dobra |
Jest wyróżnia się wbudowanym systemem mockowania, snapshot testingiem, zintegrowanym raportowaniem pokrycia kodu oraz łatwą konfiguracją z sensownymi wartościami domyślnymi.
Konfiguracja i instalacja Jest
Instalacja i inicjacja
Aby rozpocząć pracę z Jest, pierwszym krokiem jest instalacja pakietu poprzez npm lub yarn. Najprostszy sposób to wykonanie polecenia:
npm install --save-dev jest
To zainstaluje Jest jako zależność deweloperską w katalogu node_modules.
Następnie należy skonfigurować skrypt testowy w pliku package.json. Można to zrobić, dodając wpis do sekcji „scripts”:
{ "scripts": { "test": "jest" } }
Takie podejście umożliwia uruchamianie testów poprzez polecenie npm test z terminala. Można również dodać dodatkowe skrypty dla trybu watch (jest --watch) lub generowania raportów pokrycia kodu (jest --coverage).
Konfiguracja Jest
Konfigurację Jest można przechowywać na kilka sposobów. Najprościej utworzyć plik jest.config.js w katalogu głównym projektu. Alternatywnie, konfigurację można umieścić bezpośrednio w pliku package.json pod kluczem „jest”. Jest obsługuje także pliki konfiguracyjne w TypeScript (jest.config.ts) czy JSON (jest.config.json).
Przykładowa konfiguracja w JavaScript wygląda następująco:
/** @type {import('jest').Config} */
const config = {
verbose: true,
testEnvironment: 'node',
coveragePathIgnorePatterns: ['/node_modules/'],
};
module.exports = config;
Opcja verbose: true powoduje, że Jest wypisze szczegółowe informacje o każdym teście. testEnvironment określa, czy testy mają być uruchamiane w środowisku Node.js, czy w środowisku przeglądarki (JSDOM). coveragePathIgnorePatterns wskazuje katalogi ignorowane podczas obliczania pokrycia kodu.
Inne ważne opcje konfiguracyjne obejmują:
- moduleDirectories – określa katalogi, w których Jest szuka modułów;
- testMatch – definiuje wzorzec plików zawierających testy;
- collectCoverage – włącza generowanie raportów pokrycia kodu;
- setupFilesAfterEnv – wskazuje pliki uruchamiane przed każdym testem.
Integracja z IDE
Nowoczesne edytory kodu, takie jak Visual Studio Code, oferują rozszerzenia ułatwiające pracę z Jest. Rozszerzenie „Jest Test Explorer” dla VS Code umożliwia uruchamianie testów bezpośrednio z edytora. Po zainstalowaniu rozszerzenia deweloper może zobaczyć wszystkie testy w panelu bocznym i uruchomić je klikając przycisk „Run” obok testu.
Debugowanie testów w VS Code również jest możliwe. Deweloper może ustawić punkt przerwania, a następnie uruchomić test w trybie debug. Edytor wstrzyma wykonanie w punkcie przerwania, co pozwala na inspekcję zmiennych i krokowe przechodzenie przez kod.
Pisanie testów jednostkowych z Jest
Struktura testu AAA (arrange–act–assert)
Dobrze napisany test jednostkowy stosuje wzorzec arrange–act–assert (AAA), który dzieli test na trzy jasne sekcje. Sekcja arrange (przygotowanie) ustawia warunki do uruchomienia testu – inicjalizacja zmiennych, przygotowanie obiektów, mockowanie zależności. Sekcja act (akcja) wywołuje testowaną funkcję. Sekcja assert (asercja) porównuje wynik z oczekiwanym rezultatem.
Taka struktura zwiększa czytelność i ułatwia zrozumienie, co test sprawdza oraz dlaczego może się nie powieść. Przykład:
describe('Calculator', () => {
test('should add two numbers correctly', () => {
// Arrange
const num1 = 5;
const num2 = 3;
// Act
const result = add(num1, num2);
// Assert
expect(result).toBe(8);
});
});
Opisywanie testów – describe i it
Jest używa dwóch głównych funkcji do organizowania testów: describe i it (lub test, będącego aliasem). Funkcja describe tworzy grupę testów (test suite), a it (lub test) tworzy pojedynczy przypadek testowy.
Struktura zagnieżdżonych describe i it pozwala na logiczną organizację testów, szczególnie w większych projektach:
describe('User Service', () => {
describe('createUser', () => {
it('should create a new user with valid data', () => {
// test code
});
it('should throw error with invalid email', () => {
// test code
});
});
describe('deleteUser', () => {
it('should delete user by ID', () => {
// test code
});
});
});
Opisy w describe i it powinny być jasne i czytelne – w razie niepowodzenia będą widoczne w wynikach działania testów, co ułatwia identyfikację problemu.
Asercje – expect i matchery
Asercje są sercem każdego testu – to stwierdzenia, które sprawdzają, czy rzeczywisty rezultat odpowiada oczekiwanemu. Jest używa funkcji expect oraz bogatego zestawu matcherów. Najczęściej używane matchery to:
toBe– sprawdza ścisłą równość (operator===);toEqual– sprawdza głęboką równość obiektów i tablic;toStrictEqual– rygorystyczne porównanie z uwzględnieniem typów i brakujących właściwości;toBeNull,toBeUndefined,toBeDefined– weryfikują stan wartości (null,undefinedlub zdefiniowana);toBeTruthy,toBeFalsy– sprawdzają „prawdziwość” lub „fałszywość” wartości;toContain– weryfikuje, czy tablica zawiera element;toThrow– sprawdza, czy funkcja wyrzuca błąd;not– negacja dowolnego matchera, np.not.toBe;- matchery liczbowe i tekstowe – m.in.
toBeCloseTo,toBeGreaterThan,toMatch.
Przykład testów z różnymi matcherami:
describe('Asercje', () => {
test('number assertions', () => {
expect(4).toBe(4);
expect(4).toEqual(4);
expect(3.14).toBeCloseTo(3.1, 1);
expect(10).toBeGreaterThan(9);
expect(10).toBeGreaterThanOrEqual(10);
});
test('string assertions', () => {
expect('hello').toMatch(/ello/);
expect('hello world').toContain('world');
});
test('array/object assertions', () => {
expect([1, 2, 3]).toContain(2);
expect({a: 1}).toHaveProperty('a');
});
});
Asynchroniczne testowanie
Testowanie promises
Wiele operacji w JavaScript jest asynchronicznych – sięgają do API, czytają z bazy danych czy pracują z timerami. Jest obsługuje testowanie operacji asynchronicznych na kilka sposobów.
Najprościej zwrócić obiekt Promise z testu. Jeśli test zwraca Promise, Jest poczeka na jego spełnienie (resolve) lub odrzucenie (reject), zanim uzna test za zakończony:
test('fetchData resolves with data', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
Async/await w testach
Nowocześniejszym podejściem jest użycie async i await. Test może być oznaczony jako async, a następnie używać await do czekania na Promise:
test('fetchData resolves with data', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
Async/await zwiększa czytelność i naturalność testów asynchronicznych.
Testowanie obsługi błędów
Przy testowaniu operacji asynchronicznych ważne jest również badanie scenariuszy porażki. Jeśli oczekujemy, że obiekt Promise zostanie odrzucony, można użyć matchera rejects:
test('fetchData rejects with error', async () => {
await expect(fetchData()).rejects.toMatch('network error');
});
Alternatywnie, można użyć bloku try/catch:
test('fetchData handles errors', async () => {
try {
await fetchData();
} catch (error) {
expect(error).toMatch('network error');
}
});
Mockowanie i stubowanie
Czym jest mockowanie
Mockowanie to technika zastępowania rzeczywistej implementacji funkcji lub modułu wersją testową, którą możemy kontrolować. Pozwala to izolować kod testowany od jego zależności i precyzyjnie symulować scenariusze trudne do odtworzenia w testach. Zamiast używać prawdziwych zależności (np. bazy danych czy API), testujemy logikę w izolacji z użyciem mocków.
Jest ma wbudowany system mockowania, który jest intuicyjny i zaawansowany. Można mockować funkcje, moduły oraz zmienne globalne.
Mockowanie funkcji
Aby stworzyć funkcję–mock w Jest, można użyć jest.fn():
const mockFunction = jest.fn();
mockFunction.mockReturnValue(42);
const result = mockFunction();
expect(result).toBe(42);
expect(mockFunction).toHaveBeenCalled();
Funkcja–mock ma specjalny obiekt .mock, który zawiera informacje o wywołaniach:
const mockFn = jest.fn();
mockFn('first call');
mockFn('second call');
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenNthCalledWith(1, 'first call');
expect(mockFn).toHaveBeenNthCalledWith(2, 'second call');
Mockowanie modułów
Aby zamockować cały moduł, można użyć jest.mock():
jest.mock('./userService');
import { getUserById } from './userService';
test('gets user data', () => {
getUserById.mockReturnValue({ id: 1, name: 'John' });
const user = getUserById(1);
expect(user.name).toBe('John');
});
Jest automatycznie zastąpi wszystkie importy tego modułu wersją–mockiem.
Ważne uwagi o mockach
Chociaż mockowanie jest potężne, należy używać go rozsądnie. Nadmierne mockowanie prowadzi do testów dających fałszywe poczucie bezpieczeństwa – lepiej mockować tylko trudne lub zewnętrzne zależności (API, bazy danych), a logikę biznesową testować bez mocków.
Kent C. Dodds, ekspert od testowania, rekomenduje testować kod z perspektywy użytkownika (black-box), zamiast implementacyjnych detali – testuj publiczne API i zachowanie.
Testy snapshotowe
Co to jest snapshot testing
Snapshot testing to technika szczególnie przydatna do testowania komponentów UI czy dużych struktur danych. Test renderuje komponent lub wywołuje funkcję, serializuje wynik do łańcucha i zapisuje jako „snapshot”. Przy kolejnym uruchomieniu nowy snapshot jest porównywany z zapisanym. Różnice powodują niepowodzenie testu, co sygnalizuje niezamierzone zmiany.
To pomaga upewnić się, że komponenty nadal wyglądają i działają zgodnie z oczekiwaniami po zmianach w kodzie. Zmiany snapshotu są widoczne w przeglądach kodu i mogą zostać świadomie zaakceptowane.
Praktyczne zastosowanie
Aby napisać test snapshotowy w Jest, użyj matchera toMatchSnapshot():
import renderer from 'react-test-renderer';
import Link from '../Link';
it('renders correctly', () => {
const tree = renderer
.create(<Link page="https://example.com">Example</Link>)
.toJSON();
expect(tree).toMatchSnapshot();
});
Przy pierwszym uruchomieniu Jest tworzy snapshot i zapisuje go w pliku __snapshots__/Link.test.js.snap. Przy następnych uruchomieniach porównuje nowy snapshot ze starym.
Jeśli zmiana snapshotu jest zamierzona, można zaktualizować pliki przez uruchomienie testów z flagą -u lub --updateSnapshot:
jest --updateSnapshot
Wskazówki do snapshot testing
Snapshoty powinny być dodawane do repozytorium i przeglądane z należytą starannością. Dla dużych komponentów warto czasem testować konkretne właściwości zamiast całego snapshotu, aby ułatwić przegląd i uniknąć nadwrażliwości testów na nieistotne różnice.
Najlepsze praktyki w testowaniu JavaScript
Testuj zachowanie, nie implementację
Jednym z najczęstszych błędów jest testowanie szczegółów implementacji zamiast publicznego zachowania. Testy powinny być odporne na refaktoryzację – jeśli zmienia się implementacja, a publiczne API pozostaje bez zmian, testy nadal powinny przechodzić.
Zamiast tego sprawdzaj, że funkcja zwraca prawidłowy wynik dla danych wejściowych, niezależnie od wewnętrznej implementacji:
// ❌ Bad - testuje implementację
test('counter increments state', () => {
const counter = new Counter();
expect(counter.state.count).toBe(0);
counter.increment();
expect(counter.state.count).toBe(1);
});
// ✅ Good - testuje zachowanie
test('counter displays incremented number', () => {
render(<Counter />);
expect(screen.getByText('0')).toBeInTheDocument();
fireEvent.click(screen.getByText('Increment'));
expect(screen.getByText('1')).toBeInTheDocument();
});
Używaj realistycznych danych testowych
Wiele błędów produkcyjnych wynika z nieoczekiwanych danych wejściowych. Używaj realistycznych danych zamiast prostych „foo” czy „bar”.
Istnieją biblioteki generujące realistyczne dane testowe, takie jak Faker czy Chance:
// ❌ Bad - nierealistyczne dane
test('creates user', () => {
const user = createUser('foo', 'bar@baz');
expect(user.name).toBe('foo');
});
// ✅ Good - realistyczne dane
test('creates user', () => {
const user = createUser(
faker.person.fullName(),
faker.internet.email()
);
expect(user.name).toBeTruthy();
expect(user.email).toMatch(/@/);
});
Pokrycie kodu (coverage) – ostrożnie
Coverage to metryka mówiąca, ile procent linii kodu wykonują testy. Dążenie do 100% pokrycia nie zawsze ma sens – można mieć 100% coverage, ale testy niewiele sprawdzają.
80% pokrycia kodu jest często rozsądnym celem dla większości projektów. Skup się na krytycznych ścieżkach i logice biznesowej zamiast na samym wskaźniku.
Jak zauważa Kent C. Dodds:
0% coverage oznacza, że testujesz za mało, a 100% – że prawdopodobnie testujesz za dużo
Dla niewielkich bibliotek open source 100% może być zasadne; dla dużych aplikacji lepiej skupić się na ważnych częściach.
Testowanie błędów i przypadków brzegowych
Dobra test suite powinna sprawdzać nie tylko happy path, ale również scenariusze błędów i przypadki brzegowe:
describe('calculateDiscount', () => {
test('calculates discount for valid input', () => {
expect(calculateDiscount(100, 0.1)).toBe(90);
});
test('throws error for negative discount', () => {
expect(() => calculateDiscount(100, -0.1)).toThrow();
});
test('throws error for price over 100%', () => {
expect(() => calculateDiscount(100, 1.5)).toThrow();
});
});
Zaawansowane funkcje Jest
Testy parametryczne (test.each)
Testy parametryczne pozwalają uruchomić ten sam test z różnymi zestawami danych wejściowych. Zmniejszają powtarzalność i zwiększają pokrycie kombinacji przypadków.
Oto przykład z tablicą przypadków:
describe('Calculator', () => {
test.each([
[1, 2, 3],
[2, 3, 5],
[5, 5, 10],
])('add(%i, %i) = %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected);
});
});
Albo w formie czytelnej tabeli danych:
describe('Project Generator', () => {
test.each`
projectType | buildSystem | buildFile | wrapperName
${'app'} | ${'maven'} | ${'pom.xml'} | ${'mvnw'}
${'app'} | ${'gradle'} | ${'build.gradle'}| ${'gradlew'}
${'lib'} | ${'maven'} | ${'pom.xml'} | ${'mvnw'}
${'lib'} | ${'gradle'} | ${'build.gradle'}| ${'gradlew'}
`('generates $projectType with $buildSystem', ({ projectType, buildSystem, buildFile, wrapperName }) => {
// test logic
});
});
Przygotowanie i sprzątanie – beforeEach, afterEach
Czasem testy potrzebują wspólnego przygotowania lub czyszczenia. Funkcje beforeEach i afterEach uruchamiają kod przed każdym testem lub po każdym teście:
describe('Database Tests', () => {
let db;
beforeEach(() => {
// Setup - tworzenie testowej bazy danych
db = new Database();
db.initialize();
});
afterEach(() => {
// Cleanup - czyszczenie po teście
db.close();
});
test('inserts user', () => {
db.insert('users', { name: 'John' });
const user = db.find('users', { name: 'John' });
expect(user).toBeTruthy();
});
});
Istnieją również beforeAll i afterAll, które uruchamiają się raz przed wszystkimi testami lub raz po wszystkich testach.
expect.assertions
expect.assertions() zapewnia, że określona liczba asercji została wykonana. Jest to szczególnie ważne w testach asynchronicznych, gdzie łatwo zapomnieć o return lub await:
test('validatePayload', () => {
expect.assertions(2);
try {
validatePayload({ /* invalid data */ });
} catch (error) {
expect(error).toBeInstanceOf(ValidationError);
expect(error).toHaveProperty('details');
}
});
Integracja testów z potokiem CI/CD
Testowanie w continuous integration
Nowoczesne zespoły używają continuous integration (CI), gdzie kod jest testowany automatycznie przy każdym commicie. Jest łatwo integruje się z popularnymi systemami CI, takimi jak GitHub Actions, Jenkins czy GitLab CI.
Typowy potok CI obejmuje etapy: pobranie kodu z repozytorium, instalację zależności, uruchomienie testów, analizę (linting) i budowanie artefaktów. Jeśli którykolwiek etap się nie powiedzie, build jest oznaczony jako nieudany, a deweloper zostaje powiadomiony.
W środowisku CI Jest pracuje w trybie bez obserwowania zmian (non-watch) i może generować raporty w formacie XML lub JSON, które system CI potrafi zinterpretować.
Automatyzacja testów bezpieczeństwa
Oprócz testów jednostkowych i integracyjnych, nowoczesny potok CI/CD powinien zawierać testy bezpieczeństwa. Narzędzia typu SAST (Static Application Security Testing) skanują kod w poszukiwaniu znanych luk.
Monitorowanie pokrycia kodu w CI również jest ważne – potok może być skonfigurowany tak, aby przerywać build, jeśli pokrycie spada poniżej określonego progu.
Typowe błędy w testowaniu i jak ich unikać
Testowanie szczegółów implementacji
Testowanie detali implementacyjnych zamiast publicznego API prowadzi do kruchych testów. Testy powinny być odporne na refaktoryzację – dopóki publiczne API się nie zmienia, testy powinny przechodzić.
Zapominanie o async/await
Częsty błąd to zapominanie o return lub await w testach asynchronicznych. To prowadzi do fałszywych pozytywów – test „przechodzi”, choć asercja nigdy się nie wykonała, bo obiekt Promise nie został oczekany.
Nadmierne mockowanie
Mockowanie wszystkiego prowadzi do testów, które nie sprawdzają rzeczywistej współpracy modułów. Mockuj tylko zewnętrzne zależności (API, baza), a logikę testuj realnie.
Ignorowanie przypadków brzegowych
Łatwo pisać testy dla happy path, trudniej dla przypadków brzegowych i błędów. Dobra test suite zawiera testy wszystkich możliwych ścieżek – sukcesu i błędu.
Brak opisów testów
Niejasne opisy przypadków testowych utrudniają zrozumienie, zwłaszcza nowym członkom zespołu lub po dłuższej przerwie. Opisy powinny jasno mówić, co jest sprawdzane.
Zaawansowany przykład – testowanie komponentu React
Aby zilustrować praktyczne zastosowanie Jest, poniżej znajduje się przykład testowania komponentu React:
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { UserList } from './UserList';
describe('UserList Component', () => {
beforeEach(() => {
// Mock fetch
global.fetch = jest.fn();
});
afterEach(() => {
jest.restoreAllMocks();
});
test('renders loading state initially', () => {
global.fetch.mockImplementationOnce(
() => new Promise(resolve => setTimeout(resolve, 100))
);
render(<UserList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
test('displays users after loading', async () => {
const mockUsers = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
global.fetch.mockResolvedValueOnce({ json: async () => mockUsers });
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('John')).toBeInTheDocument();
expect(screen.getByText('Jane')).toBeInTheDocument();
});
});
test('handles error gracefully', async () => {
global.fetch.mockRejectedValueOnce(new Error('Network error'));
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
Ten przykład demonstruje mockowanie fetch, testowanie stanów asynchronicznych oraz użycie waitFor do oczekiwania na aktualizacje.
Narzędzia i rozszerzenia ekosystemu Jest
React Testing Library
React Testing Library (RTL) uzupełnia Jest w testowaniu komponentów React. RTL zachęca do testowania z perspektywy użytkownika – wyszukując elementy po tekście widocznym dla użytkownika zamiast po klasach CSS czy identyfikatorach.
Dzięki temu testy są bardziej odporne na refaktoryzację – zmiany stylów lub struktury DOM nie łamią testów, jeśli zachowana jest funkcjonalność i teksty.
Codecov
Codecov to narzędzie do śledzenia historii pokrycia kodu. Po każdym pushu do repozytorium analizuje raporty coverage i porównuje je z poprzednią wersją, co pomaga ocenić, czy pokrycie rośnie, czy spada.
Vitest
Vitest to nowszy framework testowy, kompatybilny z API Jest, często oferujący lepszą wydajność, szczególnie w dużych projektach. Zbudowany na Vite, zapewnia szybki tryb watch.
Zaawansowane zagadnienia
Testowanie dat i czasu
Testowanie kodu zależnego od bieżącej daty i czasu jest trudne, ponieważ czas się zmienia. Jedno z podejść to mockowanie obiektu Date lub użycie szpiegów. Lepszym podejściem bywa dependency injection – funkcja przyjmuje dateProvider jako parametr, zamiast bezpośrednio tworzyć new Date():
class DateProvider {
getDate() {
return new Date();
}
}
class UserService {
constructor(dateProvider) {
this.dateProvider = dateProvider;
}
createUser(name) {
return { name, createdAt: this.dateProvider.getDate() };
}
}
// W testach
class TestDateProvider {
constructor(date) {
this._date = new Date(date);
}
getDate() {
return this._date;
}
}
test('creates user with correct timestamp', () => {
const dateProvider = new TestDateProvider('2024-01-01');
const service = new UserService(dateProvider);
const user = service.createUser('John');
expect(user.createdAt).toEqual(new Date('2024-01-01'));
});
Testowanie error boundaries
W React, error boundaries to komponenty, które przechwytują błędy wyrzucone przez komponenty potomne. Testowanie wymaga zamaskowania console.error (by ograniczyć szum w wynikach), owinięcia testowanego komponentu w error boundary i użycia waitFor do oczekiwania na pojawienie się interfejsu zastępczego (fallback UI).
Testowanie z użyciem debuggera
Debugowanie testów bywa konieczne. W VS Code można ustawić punkt przerwania, uruchomić test w trybie debug i krokować kod. W Chrome DevTools można użyć instrukcji debugger lub logpointów do logowania bez przerywania wykonania.
Przyszłość testowania w JavaScript
Ekosystem testowania w JavaScript ewoluuje. Vitest zyskuje popularność jako alternatywa dla Jest. Playwright i Cypress są często wybierane do testów end-to-end. Na rynku pojawiają się narzędzia do automatycznego generowania testów przy użyciu AI.
Fundamenty jednak pozostają niezmienne: testy jednostkowe, TDD i dobrze napisane przypadki testowe. Niezależnie od narzędzia najważniejsze jest rozumienie, dlaczego testujemy i jak pisać testy dające realną wartość.