Temat 33 · 3 × 45 min
Testy jednostkowe
Jak sprawdzasz, czy kalkulator rat działa? Wpisujesz 1200 i 12, klikasz, patrzysz, czy jest 100. Potem zmieniasz coś w kodzie i… wpisujesz 1200 i 12 jeszcze raz. I jeszcze raz po następnej zmianie. Test jednostkowy (unit test) to ten sam sprawdzian zapisany jako kod: komputer wykonuje go w milisekundę, za każdym razem, dla dziesiątek przypadków, i krzyczy, gdy coś się zepsuje. W tej lekcji napiszesz pierwsze testy w Vitest – narzędziu z ekosystemu Vite, które poznałeś w poprzedniej lekcji – i nauczysz się pisać kod tak, by dało się go testować.
Po co testy, skoro mogę kliknąć
Oto obliczRate z lekcji 14 po drobnej „optymalizacji” kolegi:
function obliczRate(kwota, liczbaRat) {
return Math.round(kwota / liczbaRat * 100) / 100;
}
console.log(obliczRate(1200, 12)); // 100 – wygląda dobrze
console.log(obliczRate(1000, 3)); // 333.33 – też dobrze
console.log(obliczRate(1000, 0)); // Infinity – a tego nikt nie kliknął
console.log(obliczRate('1200', 12)); // 100 – „działa”, ale przez przypadek (koercja)Ręczny test sprawdza dwa, trzy przypadki i tylko dziś. Test automatyczny sprawdza dwadzieścia przypadków przy każdej zmianie kodu – także tej za pół roku, gdy nikt już nie pamięta, że liczbaRat może być zerem. Testy to też dokumentacja: czytając test('zero rat zwraca 0'), wiesz, jak funkcja ma się zachować, bez czytania jej ciała.
Pierwszy test bez żadnych narzędzi
Narzędzie do testów to w gruncie rzeczy dwie funkcje: test(nazwa, fn) uruchamia fn i raportuje, czy rzuciła błąd, a expect(wartość).toBe(oczekiwana) rzuca błąd, gdy się nie zgadza. Napiszmy je sami, żeby zdjąć z nich magię:
function test(nazwa, fn) {
try {
fn();
console.log('OK ', nazwa);
} catch (e) {
console.log('BŁĄD ', nazwa, '→', e.message);
}
}
function expect(otrzymane) {
return {
toBe(oczekiwane) {
if (otrzymane !== oczekiwane) {
throw new Error(`oczekiwano ${oczekiwane}, otrzymano ${otrzymane}`);
}
},
};
}
function obliczRate(kwota, liczbaRat) {
return Math.round(kwota / liczbaRat * 100) / 100;
}
test('1200 zł w 12 ratach to 100 zł', () => {
expect(obliczRate(1200, 12)).toBe(100);
});
test('zaokrągla do groszy', () => {
expect(obliczRate(1000, 3)).toBe(333.33);
});
test('zero rat zwraca 0, nie Infinity', () => {
expect(obliczRate(1000, 0)).toBe(0);
});Trzeci test jest czerwony – i o to chodzi. Test, który nie przechodzi, to znaleziony błąd, zanim znalazł go użytkownik. Prawdziwe narzędzie dodaje do tego ładny raport, setki matcherów, uruchamianie wielu plików i tryb obserwowania zmian.
Vitest: instalacja i pierwszy plik testów
Vitest (aktualna wersja 5; wymaga Node 22.12+ i Vite 6.4+) ma składnię zgodną z Jestem – najpopularniejszym narzędziem ostatniej dekady – więc to, czego się nauczysz, przeniesiesz wszędzie. W projekcie Vite z poprzedniej lekcji:
npm install -D vitest{
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"test:run": "vitest run"
}
}Testy trzymasz obok kodu, w plikach *.test.js (Vitest znajduje je sam):
export function obliczRate(kwota, liczbaRat) {
if (liczbaRat <= 0) return 0;
return Math.round((kwota / liczbaRat) * 100) / 100;
}import { describe, test, expect } from 'vitest';
import { obliczRate } from './raty.js';
describe('obliczRate', () => {
test('dzieli kwotę równo na raty', () => {
expect(obliczRate(1200, 12)).toBe(100);
});
test('zaokrągla do dwóch miejsc', () => {
expect(obliczRate(1000, 3)).toBe(333.33);
});
test('zero rat zwraca 0', () => {
expect(obliczRate(1000, 0)).toBe(0);
});
});npx vitest run # uruchom raz i zakończ
npm test # tryb watch: testy odpalają się po każdym zapisie plikuRaport wygląda mniej więcej tak:
✓ src/raty.test.js (3 tests) 4ms
Test Files 1 passed (1)
Tests 3 passed (3)
Duration 210msA gdy test nie przechodzi, Vitest pokazuje nazwę, linię i różnicę między Expected a Received – to pierwsza rzecz, którą czytasz.
describe, test, expect i matchery
describe(nazwa, fn)grupuje testy jednej funkcji lub jednego zachowania. Można zagnieżdżać.test(nazwa, fn)(synonim:it) to jeden przypadek. Nazwa opisuje zachowanie, nie implementację: „zero rat zwraca 0”, nie „test 3”.expect(wartość).matcher(oczekiwane)to asercja. Matcher mówi, jak porównać.
| Matcher | Sprawdza | Przykład |
|---|---|---|
toBe(x) | identyczność (===) – liczby, napisy, boolean, ta sama referencja | expect(2 + 2).toBe(4) |
toEqual(x) | tę samą strukturę obiektu/tablicy | expect(podziel('a,b')).toEqual(['a', 'b']) |
toStrictEqual(x) | jak toEqual, ale też undefined w polach i typ klasy | expect(obj).toStrictEqual({ a: 1 }) |
toBeCloseTo(x, miejsca) | liczby zmiennoprzecinkowe z tolerancją | expect(0.1 + 0.2).toBeCloseTo(0.3, 5) |
toBeTruthy() / toBeFalsy() | prawdziwość w sensie if | expect(lista.length).toBeTruthy() |
toBeNull() / toBeUndefined() | dokładnie null / undefined | expect(znajdz(99)).toBeUndefined() |
toContain(x) | element w tablicy lub fragment napisu | expect('Koszt: 50 zł').toContain('zł') |
toHaveLength(n) | .length | expect(oceny).toHaveLength(3) |
toBeGreaterThan(n) / toBeLessThan(n) | porównania | expect(srednia).toBeGreaterThan(1) |
toThrow(komunikat?) | funkcja rzuca błąd | expect(() => dodaj(7)).toThrow('zakres') |
.not. | negacja dowolnego matchera | expect(x).not.toBe(0) |
Który z tych testów przejdzie?
const wynik = podziel('a,b'); // zwraca ['a', 'b']
// A
expect(wynik).toBe(['a', 'b']);
// B
expect(wynik).toEqual(['a', 'b']);Funkcja dodajOcene(7) ma rzucić błąd. Który test to poprawnie sprawdza?
Przypadki brzegowe: tu żyją błędy
Test „1200 / 12 = 100” niczego nie wykryje – ten przypadek sprawdziłeś już ręcznie. Wartość testów bierze się z przypadków brzegowych (edge cases), których nikt nie klika. Dla każdej funkcji przejdź listę:
| Rodzaj danych | Pytania do zadania |
|---|---|
| liczby | zero? ujemna? ułamek? bardzo duża? NaN? granice zakresu (1 i 6 dla oceny, 0 i 100 dla procentu)? |
| napisy | pusty ''? same spacje? polskie znaki? bardzo długi? wielkość liter? |
| tablice | pusta []? jeden element? duplikaty? |
| kontrolki | input.value to zawsze napis – co, gdy funkcja dostanie '12' zamiast 12? |
| obiekty | brak pola? null? |
Gdy przypadków jest dużo, zamiast dziesięciu testów napisz tabelę – test.each:
import { describe, test, expect } from 'vitest';
import { srednia, slownie } from './oceny.js';
describe('srednia', () => {
test('pusta tablica daje 0, nie NaN', () => {
expect(srednia([])).toBe(0);
});
test('jedna ocena to ona sama', () => {
expect(srednia([4])).toBe(4);
});
test('zaokrągla do dwóch miejsc', () => {
expect(srednia([5, 4, 4])).toBeCloseTo(4.33, 2);
});
});
describe('slownie', () => {
test.each([
[1, 'niedostateczny'],
[2, 'dopuszczający'],
[3, 'dostateczny'],
[4, 'dobry'],
[5, 'bardzo dobry'],
[6, 'celujący'],
])('slownie(%i) → %s', (ocena, nazwa) => {
expect(slownie(ocena)).toBe(nazwa);
});
test.each([0, 7, -1, 3.5, NaN])('odrzuca %s', (zla) => {
expect(() => slownie(zla)).toThrow();
});
});Zobacz, co test brzegowy robi z funkcją nazwaPliku z lekcji 13 (wycinanie nazwy z C:\fakepath\zdjecie.png):
function nazwaPliku(sciezka) {
return sciezka.slice(sciezka.lastIndexOf('\\') + 1);
}
// przypadek z arkusza – działa
console.log(nazwaPliku('C:\\fakepath\\zdjecie.png'));
// brzegowe:
console.log(nazwaPliku('zdjecie.png')); // brak ścieżki – OK dzięki -1 + 1 = 0
console.log(nazwaPliku('')); // pusty napis – OK, ''
console.log(nazwaPliku('/home/ola/zdjecie.png')); // ścieżka z Linuksa/macOS – BŁĄD: zwraca całośćCzwarty wiersz to prawdziwy błąd, który na Windowsie nikt by nie zauważył. Test expect(nazwaPliku('/home/ola/zdjecie.png')).toBe('zdjecie.png') wyłapie go na każdym komputerze.
Mini-cykl TDD: czerwony, zielony, refaktor
TDD (Test-Driven Development) odwraca kolejność: najpierw test, potem kod. Cykl ma trzy kroki i trwa minuty, nie godziny:
- Czerwony – napisz test opisujący jedno małe zachowanie. Uruchom. Musi nie przejść (jeśli przechodzi, test niczego nie sprawdza).
- Zielony – napisz najprostszy kod, który sprawia, że test przechodzi. Nawet głupi.
- Refaktor – uporządkuj kod. Testy pilnują, że nic nie zepsułeś.
Przećwiczmy na konwersji dziesiętnej na binarną z grupowaniem po 4 cyfry (arkusz INF.03 ze stycznia 2025, lekcja 6). Oczekujemy: naBinarny(5) → '101', naBinarny(2025) → '111 1110 1001'.
Okrążenie 1. Test: expect(naBinarny(0)).toBe('0'). Czerwony (funkcji nie ma). Zielony: return '0'. Tak, naprawdę.
Okrążenie 2. Test: expect(naBinarny(5)).toBe('101'). Czerwony. Zielony: prawdziwa pętla z resztą z dzielenia.
Okrążenie 3. Test: expect(naBinarny(2025)).toBe('111 1110 1001'). Czerwony. Zielony: dopisz grupowanie od prawej.
Refaktor. Wydziel grupujOdPrawej(napis, 4) – i uruchom testy. Zielone? Gotowe.
function naBinarny(n) {
if (n === 0) return '0';
let bity = '';
while (n > 0) {
bity = (n % 2) + bity; // reszta z przodu – od razu w dobrej kolejności
n = Math.floor(n / 2);
}
return grupujOdPrawej(bity, 4);
}
function grupujOdPrawej(napis, co) {
const grupy = [];
for (let koniec = napis.length; koniec > 0; koniec -= co) {
grupy.unshift(napis.slice(Math.max(0, koniec - co), koniec));
}
return grupy.join(' ');
}
// testy z trzech okrążeń (uproszczony expect z początku lekcji)
const przypadki = [[0, '0'], [5, '101'], [16, '1 0000'], [2025, '111 1110 1001']];
for (const [wejscie, oczekiwane] of przypadki) {
const wynik = naBinarny(wejscie);
console.log(wynik === oczekiwane ? 'OK ' : 'BŁĄD ', `naBinarny(${wejscie}) → "${wynik}"`);
}Dlaczego „najprostszy kod” w okrążeniu 1? Bo to uczy pisać tylko to, czego wymaga test, a nie wszystko, co wydaje ci się potrzebne. Po trzech okrążeniach masz funkcję z czterema testami, które zostają z projektem na zawsze.
Czysta logika osobno, DOM osobno
Większość twojego kodu z lekcji 11–19 wygląda tak:
function oblicz() {
const powierzchnia = parseFloat(document.getElementById('powierzchnia').value);
const warstwy = parseInt(document.getElementById('warstwy').value);
const puszki = Math.ceil((powierzchnia * warstwy) / 4);
document.getElementById('wynik').innerHTML = 'Potrzebujesz ' + puszki + ' puszek farby';
}Nie da się tego przetestować w Vitest: document nie istnieje w Node, a funkcja nic nie zwraca. Problem nie leży w testach – leży w kodzie, który miesza czytanie z ekranu, obliczenie i pisanie na ekran. Rozdziel je:
// czysta funkcja: te same argumenty → ten sam wynik, zero DOM, zero efektów ubocznych
export function liczPuszki(powierzchnia, warstwy, wydajnosc = 4) {
// !(x > 0) łapie zero, liczby ujemne i NaN z pustego pola (NaN <= 0 to false!)
if (!(powierzchnia > 0) || !(warstwy > 0)) return 0;
return Math.ceil((powierzchnia * warstwy) / wydajnosc);
}
export function komunikat(puszki) {
if (puszki === 1) return 'Potrzebujesz 1 puszkę farby';
const jednosci = puszki % 10;
const setki = puszki % 100;
// 2–4, 22–24, 32–34… → „puszki”, ale 12–14 → „puszek”
if (jednosci >= 2 && jednosci <= 4 && (setki < 12 || setki > 14)) {
return `Potrzebujesz ${puszki} puszki farby`;
}
return `Potrzebujesz ${puszki} puszek farby`;
}import { liczPuszki, komunikat } from './farba.js';
// warstwa DOM: cienka, bez logiki – czyta, wywołuje, wpisuje
document.getElementById('oblicz').addEventListener('click', () => {
const powierzchnia = parseFloat(document.getElementById('powierzchnia').value);
const warstwy = parseInt(document.getElementById('warstwy').value, 10);
document.getElementById('wynik').textContent = komunikat(liczPuszki(powierzchnia, warstwy));
});import { describe, test, expect } from 'vitest';
import { liczPuszki, komunikat } from './farba.js';
describe('liczPuszki', () => {
test('zaokrągla w górę', () => expect(liczPuszki(10, 1)).toBe(3)); // 2.5 → 3
test('dokładna wielokrotność', () => expect(liczPuszki(8, 1)).toBe(2));
test('dwie warstwy podwajają', () => expect(liczPuszki(8, 2)).toBe(4));
test('zero powierzchni to zero puszek', () => expect(liczPuszki(0, 2)).toBe(0));
test('ujemne dane to zero', () => expect(liczPuszki(-5, 1)).toBe(0));
test('NaN z pustego pola to zero puszek', () => expect(liczPuszki(NaN, 1)).toBe(0));
});
describe('komunikat', () => {
test('liczba pojedyncza', () => expect(komunikat(1)).toBe('Potrzebujesz 1 puszkę farby'));
test('2–4 → puszki', () => expect(komunikat(3)).toBe('Potrzebujesz 3 puszki farby'));
test('5 i więcej → puszek', () => expect(komunikat(5)).toBe('Potrzebujesz 5 puszek farby'));
test('12–14 → puszek (granica!)', () => expect(komunikat(12)).toBe('Potrzebujesz 12 puszek farby'));
});Czysta funkcja (pure function) ma dwie cechy: dla tych samych argumentów zawsze zwraca ten sam wynik i nie zmienia nic poza sobą (nie dotyka DOM, nie zapisuje do localStorage, nie wywołuje fetch). Takie funkcje testuje się trywialnie. Wszystko, co dotyka świata, trzymaj w cienkiej warstwie na zewnątrz. Ten podział to najważniejsza umiejętność z całej lekcji – ważniejsza niż znajomość matcherów.
Dlaczego test tej funkcji nie uruchomi się w Vitest?
export function pokazSume(a, b) {
document.getElementById('wynik').textContent = a + b;
}Plac zabaw: mini-runner w przeglądarce
Poniższy plac zabaw ma wbudowany mały runner (describe, test, expect z najważniejszymi matcherami), więc możesz pisać testy bez instalowania czegokolwiek. W zakładce JS pierwsze 60 linii to runner – nie zmieniaj go; swoje funkcje i testy dopisuj poniżej linii // === TWÓJ KOD ===. Dwa testy celowo nie przechodzą – napraw funkcję slownie, by były zielone, a potem dopisz własne przypadki brzegowe.
<h3 style="margin:0 0 8px">Wyniki testów</h3>
<ul id="wyniki"></ul>
<p id="podsumowanie"></p>ul { list-style: none; padding: 0; margin: 0; font-family: ui-monospace, monospace; font-size: 14px; }
li { padding: 3px 8px; border-left: 4px solid #ccc; margin-bottom: 2px; }
li.ok { border-color: #2e7d4f; background: #eef8f1; }
li.blad { border-color: #b3261e; background: #fdecea; }
li.grupa { border: 0; font-weight: bold; margin-top: 8px; }
#podsumowanie { font-weight: bold; }// ===== MINI-RUNNER (nie edytuj) =====
const wynikiEl = document.getElementById('wyniki');
let licznik = { ok: 0, blad: 0 };
function dodajWiersz(tekst, klasa) {
const li = document.createElement('li');
li.textContent = tekst;
li.className = klasa;
wynikiEl.append(li);
}
function describe(nazwa, fn) {
dodajWiersz(nazwa, 'grupa');
fn();
}
function test(nazwa, fn) {
try {
fn();
licznik.ok++;
dodajWiersz('OK ' + nazwa, 'ok');
} catch (e) {
licznik.blad++;
dodajWiersz('BŁĄD ' + nazwa + ' → ' + e.message, 'blad');
}
}
function pokaz(v) { return typeof v === 'string' ? `"${v}"` : JSON.stringify(v); }
function expect(otrzymane) {
const fail = (co, oczekiwane) => {
throw new Error(`${co}: oczekiwano ${pokaz(oczekiwane)}, otrzymano ${pokaz(otrzymane)}`);
};
return {
toBe: (o) => { if (!Object.is(otrzymane, o)) fail('toBe', o); },
toEqual: (o) => { if (JSON.stringify(otrzymane) !== JSON.stringify(o)) fail('toEqual', o); },
toBeCloseTo: (o, miejsca = 2) => {
if (Math.abs(otrzymane - o) >= 10 ** -miejsca / 2) fail('toBeCloseTo', o);
},
toBeTruthy: () => { if (!otrzymane) fail('toBeTruthy', true); },
toBeFalsy: () => { if (otrzymane) fail('toBeFalsy', false); },
toContain: (o) => { if (!otrzymane.includes(o)) fail('toContain', o); },
toHaveLength: (n) => { if (otrzymane.length !== n) fail('toHaveLength', n); },
toThrow: (fragment) => {
try { otrzymane(); } catch (e) {
if (fragment && !String(e.message).includes(fragment)) fail('toThrow', fragment);
return;
}
throw new Error('toThrow: funkcja nie rzuciła błędu');
},
};
}
setTimeout(() => {
document.getElementById('podsumowanie').textContent =
`Testy: ${licznik.ok} OK, ${licznik.blad} z błędem`;
});
// ===== TWÓJ KOD =====
function srednia(oceny) {
if (oceny.length === 0) return 0;
return oceny.reduce((s, o) => s + o, 0) / oceny.length;
}
function slownie(ocena) {
const nazwy = ['niedostateczny', 'dopuszczający', 'dostateczny', 'dobry', 'bardzo dobry', 'celujący'];
if (!Number.isInteger(ocena) || ocena < 1 || ocena > 6) throw new Error('Ocena poza zakresem 1–6');
return nazwy[ocena]; // <- tu jest błąd do znalezienia
}
describe('srednia', () => {
test('pusta tablica → 0', () => expect(srednia([])).toBe(0));
test('jedna ocena', () => expect(srednia([4])).toBe(4));
test('ułamek', () => expect(srednia([5, 4, 4])).toBeCloseTo(4.33, 2));
});
describe('slownie', () => {
test('1 → niedostateczny', () => expect(slownie(1)).toBe('niedostateczny'));
test('6 → celujący', () => expect(slownie(6)).toBe('celujący'));
test('0 rzuca błąd', () => expect(() => slownie(0)).toThrow('zakres'));
test('3.5 rzuca błąd', () => expect(() => slownie(3.5)).toThrow());
});Po naprawie (nazwy[ocena - 1]) dopisz grupę describe('nazwaPliku', …) z funkcją z poprzedniej sekcji i testem dla ścieżki /home/ola/zdjecie.png – a potem popraw funkcję tak, by przechodził.
Pułapki
Ściąga
| Składnia | Co robi | Przykład |
|---|---|---|
npm install -D vitest | instalacja | w projekcie Vite |
npx vitest run / npx vitest | raz / tryb watch | skrypt "test": "vitest" |
*.test.js | pliki, które Vitest znajduje sam | raty.test.js obok raty.js |
import { describe, test, expect } from 'vitest' | import API | na górze pliku testów |
describe(nazwa, fn) | grupa testów | describe('obliczRate', () => {…}) |
test(nazwa, fn) / it | jeden przypadek | test('zero rat → 0', () => {…}) |
test.each([...])(nazwa, fn) | tabela przypadków | test.each([[1,'a'],[2,'b']])('f(%i)', (we, wy) => …) |
expect(x).toBe(y) | === (prymitywy) | toBe(100) |
expect(x).toEqual(y) | struktura (obiekty, tablice) | toEqual(['a','b']) |
expect(x).toBeCloseTo(y, n) | ułamki | toBeCloseTo(4.33, 2) |
expect(() => f()).toThrow('tekst') | rzuca błąd | funkcja w środku! |
expect(x).not.toBe(y) | negacja | not.toContain('błąd') |
beforeEach(fn) | przygotowanie przed każdym testem | świeże dane |
| czerwony → zielony → refaktor | cykl TDD | test najpierw |
| czysta funkcja | argumenty → wynik, bez DOM | testuj ją, nie onclick |
Zadania
Pięć testów do obliczKoszt
Funkcja obliczKoszt(cenaZaKg, kg, rabatProcent) z lekcji 14 zwraca koszt po rabacie zaokrąglony do groszy; rabat spoza zakresu 0–100 traktuje jak 0. Napisz w Vitest (lub w mini-runnerze) co najmniej pięć testów: przypadek typowy, brak rabatu, rabat 100 %, rabat 150 (ignorowany), zero kilogramów. Potem napisz funkcję tak, by wszystkie przeszły.
Pokaż rozwiązanie
import { describe, test, expect } from 'vitest';
import { obliczKoszt } from './koszt.js';
describe('obliczKoszt', () => {
test('typowy przypadek: 12 zł/kg × 2,5 kg bez rabatu', () => {
expect(obliczKoszt(12, 2.5, 0)).toBe(30);
});
test('rabat 10 % od 30 zł daje 27 zł', () => {
expect(obliczKoszt(12, 2.5, 10)).toBe(27);
});
test('rabat 100 % daje 0', () => {
expect(obliczKoszt(12, 2.5, 100)).toBe(0);
});
test('rabat 150 jest ignorowany', () => {
expect(obliczKoszt(12, 2.5, 150)).toBe(30);
});
test('rabat ujemny jest ignorowany', () => {
expect(obliczKoszt(10, 1, -20)).toBe(10);
});
test('zero kilogramów to zero złotych', () => {
expect(obliczKoszt(12, 0, 10)).toBe(0);
});
test('zaokrągla do groszy', () => {
expect(obliczKoszt(9.99, 3, 15)).toBe(25.47); // 29.97 * 0.85 = 25.4745
});
});function obliczKoszt(cenaZaKg, kg, rabatProcent) {
const rabat = rabatProcent >= 0 && rabatProcent <= 100 ? rabatProcent : 0;
const koszt = cenaZaKg * kg * (1 - rabat / 100);
return Math.round(koszt * 100) / 100;
}
// szybka weryfikacja tych samych przypadków
console.log(obliczKoszt(12, 2.5, 0), obliczKoszt(12, 2.5, 10), obliczKoszt(12, 2.5, 100));
console.log(obliczKoszt(12, 2.5, 150), obliczKoszt(10, 1, -20), obliczKoszt(12, 0, 10));
console.log(obliczKoszt(9.99, 3, 15));Napraw nazwaPliku testami
Napisz testy dla nazwaPliku(sciezka): C:\fakepath\zdjecie.png → zdjecie.png, zdjecie.png → zdjecie.png, /home/ola/zdjecie.png → zdjecie.png, C:\a\b\c.txt → c.txt, pusty napis → pusty napis, folder/ → pusty napis. Uruchom na wersji z lekcji (lastIndexOf('\\')) – które testy są czerwone? Popraw funkcję, by obsługiwała oba separatory.
Pokaż rozwiązanie
Czerwone są testy ze ścieżką uniksową (/home/ola/zdjecie.png zwraca całość, folder/ zwraca folder/). Poprawka: szukaj ostatniego z obu separatorów.
function nazwaPliku(sciezka) {
const ostatni = Math.max(sciezka.lastIndexOf('\\'), sciezka.lastIndexOf('/'));
return sciezka.slice(ostatni + 1);
}
const przypadki = [
['C:\\fakepath\\zdjecie.png', 'zdjecie.png'],
['zdjecie.png', 'zdjecie.png'],
['/home/ola/zdjecie.png', 'zdjecie.png'],
['C:\\a\\b\\c.txt', 'c.txt'],
['', ''],
['folder/', ''],
];
for (const [we, wy] of przypadki) {
const wynik = nazwaPliku(we);
console.log(wynik === wy ? 'OK ' : 'BŁĄD ', JSON.stringify(we), '→', JSON.stringify(wynik));
}import { test, expect } from 'vitest';
import { nazwaPliku } from './nazwa-pliku.js';
test.each([
['C:\\fakepath\\zdjecie.png', 'zdjecie.png'],
['zdjecie.png', 'zdjecie.png'],
['/home/ola/zdjecie.png', 'zdjecie.png'],
['C:\\a\\b\\c.txt', 'c.txt'],
['', ''],
['folder/', ''],
])('nazwaPliku(%s) → %s', (sciezka, oczekiwana) => {
expect(nazwaPliku(sciezka)).toBe(oczekiwana);
});TDD: walidacja hasła
Metodą TDD (po jednym teście na okrążenie!) zbuduj ocenHaslo(haslo), która zwraca tablicę komunikatów o błędach (pustą, gdy hasło jest poprawne). Wymagania wprowadzaj w kolejności: (1) co najmniej 8 znaków → 'Co najmniej 8 znaków'; (2) wielka litera → 'Co najmniej jedna wielka litera'; (3) cyfra → 'Co najmniej jedna cyfra'; (4) bez spacji → 'Bez spacji'. Zapisz po każdym okrążeniu, który test był czerwony i jaka najmniejsza zmiana zrobiła go zielonym.
Pokaż rozwiązanie
Okrążenia: (1) test ocenHaslo('abc') zawiera komunikat o długości → najprostszy kod: return haslo.length < 8 ? ['Co najmniej 8 znaków'] : []. (2) test ocenHaslo('abcdefgh') zawiera komunikat o wielkiej literze → dopisanie warunku z /[A-Z]/. (3) cyfra, (4) spacja – analogicznie. Refaktor: tablica reguł zamiast czterech if.
function ocenHaslo(haslo) {
const reguly = [
[(h) => h.length >= 8, 'Co najmniej 8 znaków'],
[(h) => /[A-Z]/.test(h), 'Co najmniej jedna wielka litera'],
[(h) => /[0-9]/.test(h), 'Co najmniej jedna cyfra'],
[(h) => !/\s/.test(h), 'Bez spacji'],
];
return reguly.filter(([sprawdz]) => !sprawdz(haslo)).map(([, komunikat]) => komunikat);
}
console.log(ocenHaslo('abc'));
console.log(ocenHaslo('abcdefgh'));
console.log(ocenHaslo('Abcdefgh'));
console.log(ocenHaslo('Abcdefg 1'));
console.log(ocenHaslo('Abcdefg1')); // [] – poprawneimport { describe, test, expect } from 'vitest';
import { ocenHaslo } from './haslo.js';
describe('ocenHaslo', () => {
test('za krótkie', () => {
expect(ocenHaslo('abc')).toContain('Co najmniej 8 znaków');
});
test('brak wielkiej litery', () => {
expect(ocenHaslo('abcdefgh')).toContain('Co najmniej jedna wielka litera');
});
test('brak cyfry', () => {
expect(ocenHaslo('Abcdefgh')).toContain('Co najmniej jedna cyfra');
});
test('spacja w środku', () => {
expect(ocenHaslo('Abcdefg 1')).toContain('Bez spacji');
});
test('poprawne hasło daje pustą tablicę', () => {
expect(ocenHaslo('Abcdefg1')).toEqual([]);
});
test('kilka błędów naraz', () => {
expect(ocenHaslo('a b')).toHaveLength(4);
});
test('dokładnie 8 znaków wystarcza (granica)', () => {
expect(ocenHaslo('Abcdefg1')).not.toContain('Co najmniej 8 znaków');
});
});Refaktor kalkulatora z arkusza pod testy
Weź kalkulator kursów z arkusza INF.03 ze stycznia 2025 (lekcja 14): trzy checkboxy kursów (ceny przyjmij umownie: 300, 450, 600 zł), lista miast, pole liczby rat; wynik Kurs odbędzie się w <miasto>. Koszt całkowity: <kwota> zł. Płacisz <n> rat po <rata> zł. Twój dotychczasowy kod to jedna funkcja oblicz() czytająca DOM. Przeprowadź refaktor:
kursy.js– czyste funkcjekosztKursow(zaznaczone, cennik),rata(koszt, liczbaRat),komunikat(miasto, koszt, liczbaRat).main.js– cienka warstwa DOM.kursy.test.js– co najmniej 8 testów, w tym: brak zaznaczonych kursów, wszystkie kursy, 0 rat, 1 rata, rata nieokrągła (np. 750 / 4), komunikat dla dwóch różnych miast (tak jak sprawdza egzaminator).
Upewnij się, że działanie strony się nie zmieniło, a npx vitest run jest zielony.
Pokaż rozwiązanie
export const CENNIK = { excel: 300, python: 450, grafika: 600 };
export function kosztKursow(zaznaczone, cennik = CENNIK) {
return zaznaczone.reduce((suma, klucz) => suma + (cennik[klucz] ?? 0), 0);
}
export function rata(koszt, liczbaRat) {
if (liczbaRat <= 0 || !Number.isFinite(liczbaRat)) return 0;
return Math.round((koszt / liczbaRat) * 100) / 100;
}
export function komunikat(miasto, koszt, liczbaRat) {
return `Kurs odbędzie się w ${miasto}. Koszt całkowity: ${koszt} zł. Płacisz ${liczbaRat} rat po ${rata(koszt, liczbaRat)} zł`;
}import { kosztKursow, rata, komunikat } from './kursy.js';
document.getElementById('oblicz').addEventListener('click', () => {
const zaznaczone = [...document.querySelectorAll('input[name=kurs]:checked')].map((c) => c.value);
const miasto = document.getElementById('miasto').value;
const liczbaRat = parseInt(document.getElementById('raty').value, 10);
const koszt = kosztKursow(zaznaczone);
document.getElementById('wynik').textContent = komunikat(miasto, koszt, liczbaRat);
});import { describe, test, expect } from 'vitest';
import { kosztKursow, rata, komunikat } from './kursy.js';
describe('kosztKursow', () => {
test('nic nie zaznaczone → 0', () => expect(kosztKursow([])).toBe(0));
test('jeden kurs', () => expect(kosztKursow(['python'])).toBe(450));
test('wszystkie kursy', () => expect(kosztKursow(['excel', 'python', 'grafika'])).toBe(1350));
test('nieznany klucz jest ignorowany', () => expect(kosztKursow(['excel', 'latin'])).toBe(300));
});
describe('rata', () => {
test('0 rat → 0 (nie Infinity)', () => expect(rata(900, 0)).toBe(0));
test('1 rata to cała kwota', () => expect(rata(900, 1)).toBe(900));
test('rata nieokrągła zaokrąglona do groszy', () => expect(rata(750, 4)).toBe(187.5));
test('1000 / 3 → 333.33', () => expect(rata(1000, 3)).toBe(333.33));
test('NaN rat → 0', () => expect(rata(900, NaN)).toBe(0));
});
describe('komunikat', () => {
test('format dla Krakowa', () => {
expect(komunikat('Krakowie', 900, 3)).toBe(
'Kurs odbędzie się w Krakowie. Koszt całkowity: 900 zł. Płacisz 3 rat po 300 zł',
);
});
test('format dla Gdańska – drugi zestaw danych', () => {
expect(komunikat('Gdańsku', 1350, 10)).toBe(
'Kurs odbędzie się w Gdańsku. Koszt całkowity: 1350 zł. Płacisz 10 rat po 135 zł',
);
});
});Szybka weryfikacja logiki w konsoli (bez Vitest):
const CENNIK = { excel: 300, python: 450, grafika: 600 };
const kosztKursow = (zaznaczone) => zaznaczone.reduce((s, k) => s + (CENNIK[k] ?? 0), 0);
const rata = (koszt, n) => (n <= 0 || !Number.isFinite(n) ? 0 : Math.round((koszt / n) * 100) / 100);
const komunikat = (miasto, koszt, n) =>
`Kurs odbędzie się w ${miasto}. Koszt całkowity: ${koszt} zł. Płacisz ${n} rat po ${rata(koszt, n)} zł`;
console.log(kosztKursow([]), kosztKursow(['excel', 'python', 'grafika']));
console.log(rata(900, 0), rata(750, 4), rata(1000, 3));
console.log(komunikat('Krakowie', 900, 3));
console.log(komunikat('Gdańsku', 1350, 10));Zwróć uwagę: test komunikatu dla dwóch miast odpowiada temu, co robi egzaminator – „sprawdzane dla dwóch różnych wartości”. Egzaminator klika; ty masz to zapisane na stałe.
Test powtórkowy
Test powtórkowy
Dziewięć pytań o testy jednostkowe. Jedna poprawna odpowiedź.
Co sprawdza ten test?
test('mnoży', () => {
expect(pomnoz(2, 3)).toBe(6);
});Którego matchera użyjesz, by porównać wynik funkcji zwracającej obiekt { imie: 'Ola', wiek: 17 }?
Który test wykryje błąd off-by-one w funkcji ostatni(tablica), która ma zwracać ostatni element, a zwraca tablica[tablica.length]?
Dlaczego ten test jest zły?
test('rzuca dla zera', () => {
expect(podziel(10, 0)).toThrow();
});Jaka jest poprawna kolejność kroków w TDD?
Która funkcja jest czysta (łatwa do przetestowania)?
Które polecenie uruchamia testy jednorazowo (bez trybu obserwowania)?
Co wypisze test dla 0.1 + 0.2?
test('suma', () => {
expect(0.1 + 0.2).toBe(0.3);
});Co robi test.each?