Przejdź do treści

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:

raty.js
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)
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

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ę:

mini-test.js
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);
});
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

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:

Terminal
npm install -D vitest
package.json (fragment)
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest",
    "test:run": "vitest run"
  }
}

Testy trzymasz obok kodu, w plikach *.test.js (Vitest znajduje je sam):

src/raty.js
export function obliczRate(kwota, liczbaRat) {
  if (liczbaRat <= 0) return 0;
  return Math.round((kwota / liczbaRat) * 100) / 100;
}
src/raty.test.js
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);
  });
});
Terminal
npx vitest run        # uruchom raz i zakończ
npm test              # tryb watch: testy odpalają się po każdym zapisie pliku

Raport wygląda mniej więcej tak:

Tekst
 ✓ src/raty.test.js (3 tests) 4ms

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Duration  210ms

A 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ć.
MatcherSprawdzaPrzykład
toBe(x)identyczność (===) – liczby, napisy, boolean, ta sama referencjaexpect(2 + 2).toBe(4)
toEqual(x)tę samą strukturę obiektu/tablicyexpect(podziel('a,b')).toEqual(['a', 'b'])
toStrictEqual(x)jak toEqual, ale też undefined w polach i typ klasyexpect(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 ifexpect(lista.length).toBeTruthy()
toBeNull() / toBeUndefined()dokładnie null / undefinedexpect(znajdz(99)).toBeUndefined()
toContain(x)element w tablicy lub fragment napisuexpect('Koszt: 50 zł').toContain('zł')
toHaveLength(n).lengthexpect(oceny).toHaveLength(3)
toBeGreaterThan(n) / toBeLessThan(n)porównaniaexpect(srednia).toBeGreaterThan(1)
toThrow(komunikat?)funkcja rzuca błądexpect(() => dodaj(7)).toThrow('zakres')
.not.negacja dowolnego matcheraexpect(x).not.toBe(0)
Sprawdź się

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']);
Sprawdź się

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 danychPytania do zadania
liczbyzero? ujemna? ułamek? bardzo duża? NaN? granice zakresu (1 i 6 dla oceny, 0 i 100 dla procentu)?
napisypusty ''? same spacje? polskie znaki? bardzo długi? wielkość liter?
tablicepusta []? jeden element? duplikaty?
kontrolkiinput.value to zawsze napis – co, gdy funkcja dostanie '12' zamiast 12?
obiektybrak pola? null?

Gdy przypadków jest dużo, zamiast dziesięciu testów napisz tabelę – test.each:

src/oceny.test.js
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):

nazwa-pliku.js
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ść
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

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:

Cykl TDD: czerwony – napisz test, który nie przechodzi; zielony – napisz najprostszy kod, który go spełnia; refaktor – popraw kod, testy nadal zielone; strzałka wraca do czerwonego
Rys. 1. Cykl TDD. Każde okrążenie dodaje jedno małe zachowanie.
  1. Czerwony – napisz test opisujący jedno małe zachowanie. Uruchom. Musi nie przejść (jeśli przechodzi, test niczego nie sprawdza).
  2. Zielony – napisz najprostszy kod, który sprawia, że test przechodzi. Nawet głupi.
  3. 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.

binarny.js
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}"`);
}
Konsola
Kliknij „Uruchom”, aby zobaczyć 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:

kalkulator-przed.js
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:

farba.js
// 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`;
}
main.js
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));
});
farba.test.js
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.

Sprawdź się

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.

Mini-runner testów
// ===== 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());
});
Podgląd

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ładniaCo robiPrzykład
npm install -D vitestinstalacjaw projekcie Vite
npx vitest run / npx vitestraz / tryb watchskrypt "test": "vitest"
*.test.jspliki, które Vitest znajduje samraty.test.js obok raty.js
import { describe, test, expect } from 'vitest'import APIna górze pliku testów
describe(nazwa, fn)grupa testówdescribe('obliczRate', () => {…})
test(nazwa, fn) / itjeden przypadektest('zero rat → 0', () => {…})
test.each([...])(nazwa, fn)tabela przypadkówtest.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łamkitoBeCloseTo(4.33, 2)
expect(() => f()).toThrow('tekst')rzuca błądfunkcja w środku!
expect(x).not.toBe(y)negacjanot.toContain('błąd')
beforeEach(fn)przygotowanie przed każdym testemświeże dane
czerwony → zielony → refaktorcykl TDDtest najpierw
czysta funkcjaargumenty → wynik, bez DOMtestuj ją, nie onclick

Zadania

Zadanie 1
łatwe

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
koszt.test.js
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
  });
});
koszt.js
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));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Zadanie 2
średnie

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.

nazwa-pliku.js
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));
}
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
nazwa-pliku.test.js
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);
});
Zadanie 3
średnie

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.

haslo.js
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'));   // [] – poprawne
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
haslo.test.js
import { 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');
  });
});
Zadanie 4
trudne

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:

  1. kursy.js – czyste funkcje kosztKursow(zaznaczone, cennik), rata(koszt, liczbaRat), komunikat(miasto, koszt, liczbaRat).
  2. main.js – cienka warstwa DOM.
  3. 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
kursy.js
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ł`;
}
main.js
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);
});
kursy.test.js
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):

kursy-check.js
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));
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

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ź.

0/0poprawnych
Sprawdź się

Co sprawdza ten test?

test('mnoży', () => {
expect(pomnoz(2, 3)).toBe(6);
});
Sprawdź się

Którego matchera użyjesz, by porównać wynik funkcji zwracającej obiekt { imie: 'Ola', wiek: 17 }?

Sprawdź się

Który test wykryje błąd off-by-one w funkcji ostatni(tablica), która ma zwracać ostatni element, a zwraca tablica[tablica.length]?

Sprawdź się

Dlaczego ten test jest zły?

test('rzuca dla zera', () => {
expect(podziel(10, 0)).toThrow();
});
Sprawdź się

Jaka jest poprawna kolejność kroków w TDD?

Sprawdź się

Która funkcja jest czysta (łatwa do przetestowania)?

Sprawdź się

Które polecenie uruchamia testy jednorazowo (bez trybu obserwowania)?

Sprawdź się

Co wypisze test dla 0.1 + 0.2?

test('suma', () => {
expect(0.1 + 0.2).toBe(0.3);
});
Sprawdź się

Co robi test.each?