Testiranje softvera · Poglavlje 5 · Sedmica 5

Jedinično testiranje

Dio 1 – Dobar jedinični test

Dio 2 – Alati pytesta

Dio 3 – Razvoj vođen testovima

Praksa – TDD u DeliveryApp

Materijale priredio Erkan Islamović

erkan.islamovic@eubd.edu.ba · erkan.islamovic@gmail.com

Šta učimo ovog semestra

Petnaest sedmica: svaka ima tri časa teorije i tri časa praktične nastave. Danas: poglavlje 5 (Sedmica 5, označeno). Isti primjer kroz knjigu: DeliveryApp, narudžba N-1001.

Sed.PoglavljeTeorija (3 časa)Praksa (3 časa)
1Zašto testiramoUvodne priče; greška, defekt, otkaz; sedam principa; DeliveryAppTabela očekivanih cijena rukom
2Proces i nivoiTest proces; nivoi i V-model; vrste; statičko testiranjeRecenzija zahtjeva DeliveryApp
3Manuelno testiranjeTest slučaj; izvještaj o defektu; istraživačko testiranjeTest slučajevi za web stranicu
4Automatizovano testiranjeŠta automatizovati; piramida testova; prvi testPrvi pytest
5Jedinično testiranjeAAA; fixture, parametri, izuzeci; TDDTestovi za dostavu i popust
6Bijela kutijaGraf toka; pokrivenost; ciklomatska složenostcoverage.py do 100 % grana
7Tok podatakaDefinicija i upotreba; anomalije; statička analiza, mutacijeruff, mypy, ručna mutacija
8Crna kutijaEkvivalencija i granice; tabela odlučivanja; stanjaLov na pet grešaka

Šta učimo ovog semestra

Sed.PoglavljeTeorija (3 časa)Praksa (3 časa)
9Model i integracijaModel; strategije integracije; APITestClient za API
10Sistemsko testiranjeSistemsko; performanse i sigurnost; prihvatno i BDDLocust, Gherkin
11OO testiranjeStanje objekta; nasljeđivanje; dvojniciNarudzba, unittest.mock
12Paralelne aplikacijeTrka podataka; zastoj; nestabilni testoviNiti, 1000 ponavljanja
13Web aplikacijeWeb i DOM; Playwright; Page Object, pristupačnostPlaywright za DeliveryApp
14Upravljanje testiranjemTest plan i rizik; metrike; CI/CDGitHub Actions
15Alati, AI, projekatPregled alata; AI u testiranju; ISTQBOdbrana projekta
N-1001 je 24,50 € · od 30,00 € dostava je besplatna

Literatura

  1. J. Popović: Priručnik za test inženjere, CET i Računarski fakultet, Beograd, 2023.
  2. Rakić-Skoković: Priručnik za testiranje softvera, Fakultet tehničkih nauka, Novi Sad, 2013.
  3. G. J. Myers: The Art of Software Testing, 2. izdanje, John Wiley & Sons, 2004.
  4. A. P. Mathur: Foundations of Software Testing, Addison-Wesley Professional, 2008.
Priča za početak · Sedmica 5

Priča za početak

Priča za početak

U jesen 1997. godine dva programera sjela su u avion od Ciriha do Atlante, na putu na veliku konferenciju o objektnom programiranju OOPSLA. Jedan je bio Kent Beck, Amerikanac koji je nekoliko godina ranije za jezik Smalltalk napisao mali alat za testiranje. Drugi je bio Erich Gamma, Švicarac, jedan od četiri autora slavne knjige Design Patterns. Beck je htio naučiti Javu, a Gamma je htio vidjeti Beckov alat za testiranje. Umjesto da gledaju filmove, otvorili su laptop i programirali u paru, nekoliko sati, iznad Atlantika. Pisali su na način koji je Beck propovijedao: prvo test, pa kod koji ga zadovolji.

Kad je avion sletio, imali su prvu verziju alata JUnit. Taj mali alat, napravljen tokom jednog leta, postao je jedan od najkorištenijih programa u historiji Jave, a po njegovom uzoru nastali su alati za skoro svaki jezik: NUnit za C#, PHPUnit, CppUnit, unittest za Python. Cijela porodica zove se xUnit. Pouka priče nije samo da se dobre stvari mogu napraviti brzo. Pouka je da je jedinični test toliko jednostavna ideja da stane u let avionom: mali program koji provjeri mali dio drugog programa, i to radi svaki put kad ga pozovete.

Zašto ovo poglavlje

U poglavlju 4 napisali smo prve automatske testove u pytestu i vidjeli piramidu testova: najviše testova treba biti u podnožju, na nivou jedinice. Ovo poglavlje uči kako se ti testovi pišu dobro. Prvo vidimo šta je jedinica i kako izgleda dobar jedinični test (obrazac pripremi–izvrši–provjeri). Zatim alate pytesta koji štede posao: parametrizaciju, provjeru izuzetaka i fixture. Na kraju upoznajemo razvoj vođen testovima (TDD), način rada iz Beckovog aviona, i njime pravimo novu funkciju DeliveryApp: napojnicu za dostavljača.

Riječi ovog poglavlja (1/2)

  • Jedinica – najmanji dio programa koji ima smisla testirati odvojeno: funkcija, metoda ili klasa.
  • Jedinični test – test jedne jedinice, izolovano od ostatka sistema (baze, mreže, ekrana).
  • Pripremi–izvrši–provjeri (Arrange–Act–Assert, AAA) – tri dijela svakog dobrog testa.
  • Parametrizacija – jedan test izvršen više puta s različitim podacima.
  • Izuzetak (exception) – greška koju funkcija namjerno javi kad ulaz nije dozvoljen (u Pythonu raise ValueError).
  • Fixture – pripremljeno stanje ili objekat koji pytest daje testu (npr. prihvaćena narudžba).

Riječi ovog poglavlja (2/2)

  • Razvoj vođen testovima (Test-Driven Development, TDD) – način rada u kojem se prvo napiše test koji pada, pa kod koji ga zadovolji, pa se kod uredi.
  • Crveno–zeleno–refaktoriši – tri koraka TDD-a: test pada, test prolazi, kod se uredi bez mijenjanja ponašanja.
  • Refaktorisanje – mijenjanje unutrašnjosti koda radi jasnoće, bez promjene onoga što kod radi.
  • Test dvojnik (test double) – zamjena za dio sistema koji ne želimo zvati u testu (baza, servis plaćanja); detaljno u poglavlju 9.
  • xUnit – porodica alata za jedinično testiranje nastalih po uzoru na SUnit i JUnit.

Ciljevi poglavlja

Nakon poglavlja možete:

reći šta je jedinica i zašto se testira izolovano; napisati jedinični test po obrascu pripremi–izvrši–provjeri; dati testu ime koje kaže šta provjerava; jednim parametrizovanim testom provjeriti više vrijednosti; provjeriti da funkcija javlja izuzetak za nedozvoljen ulaz; napraviti fixture za zajedničku pripremu; objasniti zašto se novac čuva u centima, a ne u decimalnim brojevima; objasniti ciklus crveno–zeleno–refaktoriši; razviti funkciju napojnice postupkom TDD, korak po korak; reći šta jedinični testovi ne mogu otkriti.

Plan poglavlja – Sedmica 5: tri časa teorije i tri časa praktične nastave

ČasSadržajNa času
Teorija 15.1–5.3 Jedinica; pripremi–izvrši–provjeri; imena testovaTest klase Narudzba
Teorija 25.4–5.6 Parametrizacija; izuzeci; fixture; zašto centi0.1 + 0.2 ≠ 0.3
Teorija 35.7–5.9 TDD; napojnica korak po korak; granice jediničnih testovaCrveno, zeleno, refaktoriši uživo
Dio 1 · Sedmica 5

Dio 1 – Dobar jedinični test

5.1 Šta je jedinica

Jedinica je najmanji dio programa koji ima smisla testirati odvojeno. U Pythonu je to najčešće jedna funkcija (cijena_dostave, popust) ili jedna klasa s metodama (Narudzba). Jedinični test provjerava jednu jedinicu, izolovano: bez baze podataka, bez mreže, bez ekrana, bez drugih servisa.

Zašto izolovano? Tri razloga:

  1. Brzina. Funkcija cijena_dostave izvrši se za milionitinke sekunde. Ako bi test išao kroz bazu i mrežu, trajao bi stotine puta duže, i 500 testova bi trajalo minutama umjesto desetinke sekunde.
  2. Tačno mjesto greške. Kad padne jedinični test funkcije popust, greška je u funkciji popust. Kad padne test kroz web stranicu, greška može biti bilo gdje.
  3. Ponovljivost. Funkcija koja samo računa daje isti rezultat svaki put. Baza može biti prazna, mreža spora, servis nedostupan.

Zato je u DeliveryApp pravilo odvojeno od svega ostalog: funkcije cijena_dostave, popust i ukupno ne znaju ništa o bazi ni o ekranu; dobiju brojeve i vrate broj. Ovakav dizajn se zove testabilan: lako ga je testirati. Kad je kod pisan tako da pravilo, baza i ekran žive zajedno u jednoj funkciji od 200 redova, jedinično testiranje postaje teško, i to je znak lošeg dizajna, a ne lošeg testera.

5.1 Šta je jedinica

Slika iz života: kontrola kvaliteta u fabrici bicikala. Svaki dio (kočnica, lanac, točak) testira se na posebnom stolu, prije sklapanja. Ako kočnica ne valja, zna se odmah da je kočnica, a ne „nešto na biciklu”.

Ukratko – 5.1 Šta je jedinica

  • Jedinica je najmanji dio programa koji se testira odvojeno: funkcija, metoda ili klasa.
  • Jedinični test je izolovan od baze, mreže i ekrana, pa je brz, pokazuje tačno mjesto greške i ponovljiv je.
  • Kod u kojem je pravilo odvojeno od baze i ekrana je testabilan.
  • Kad je jedinično testiranje teško, to je obično znak lošeg dizajna.

5.2 Pripremi–izvrši–provjeri

Skoro svaki dobar test ima tri dijela, uvijek istim redom:

  1. Pripremi (Arrange): napravi ulaz i stanje koje test treba.
  2. Izvrši (Act): pozovi jednu radnju koju testiraš.
  3. Provjeri (Assert): uporedi rezultat s očekivanim.

Primjer: test da se narudžba u pripremi ne može otkazati (PP-6).

5.2 Pripremi–izvrši–provjeri

import pytest
from deliveryapp import Narudzba

def test_u_pripremi_se_ne_moze_otkazati():
    # Pripremi: narudžba koju je restoran prihvatio i počeo pripremati
    n = Narudzba("N-1001")
    n.promijeni("PRIHVAĆENA")
    n.promijeni("U_PRIPREMI")

    # Izvrši i provjeri: otkazivanje mora javiti grešku
    with pytest.raises(ValueError):
        n.promijeni("OTKAZANA")

    # Provjeri: stanje se nije promijenilo
    assert n.stanje == "U_PRIPREMI"

5.2 Pripremi–izvrši–provjeri

Prazni redovi između tri dijela nisu ukras. Oni pokazuju čitaocu gdje se šta dešava, i test se čita kao mala priča: „imamo narudžbu u pripremi; pokušamo je otkazati; to ne smije uspjeti, i ona ostaje u pripremi.”

Ovaj test, pokrenut nad prvom verzijom DeliveryApp (DeliveryApp=v0), pada. U prvoj verziji je dozvoljen prelaz iz U_PRIPREMI u OTKAZANA (defekt D-04), pa izuzetak ne bude javljen. pytest kaže:

FAILED tests/test_narudzba.py::test_u_pripremi_se_ne_moze_otkazati - Failed: DID NOT RAISE ValueError

„DID NOT RAISE” znači: očekivali smo grešku, a nije je bilo. Defekt je nađen jednim testom od deset redova, za milisekundu.

Slika 5.1 – Tri dijela jediničnog testa
Slika 5.1 – Tri dijela jediničnog testa

Ukratko – 5.2 Pripremi–izvrši–provjeri

  • Dobar test ima tri dijela: pripremi, izvrši, provjeri.
  • Izvršava se jedna radnja, a prazni redovi odvajaju dijelove.
  • Test „narudžba u pripremi se ne može otkazati” pada na prvoj verziji s porukom DID NOT RAISE i tako otkriva D-04.

5.3 Ime i jedna stvar

Kad test padne na serveru za kontinuiranu integraciju u dva ujutro, programer ujutro vidi samo njegovo ime i poruku. Zato ime mora reći šta se provjerava i šta se očekuje:

5.3 Ime i jedna stvar

Loše imeDobro ime
test1test_n1001_ukupno_24_50
test_dostavatest_dostava_besplatna_od_30_eura
test_narudzba_raditest_u_pripremi_se_ne_moze_otkazati
test_popust_2test_kod_malim_slovima_daje_popust

Drugo pravilo: jedan test provjerava jedno ponašanje. Test koji provjerava dostavu, popust, otkazivanje i ispis cijene u jednom ima dvije mane. Prvo, kad padne, ne zna se šta je pokvareno. Drugo, prvi assert koji padne zaustavi test, pa ostale provjere uopšte nisu izvršene, i ne znamo da li je i tamo nešto pokvareno.

To ne znači da test smije imati samo jedan assert. Test test_prihvacena_moze_u_pripremu provjerava i stanje i istoriju, jer su to dva dijela istog ponašanja („prelaz je zapisan”). Pravilo je jedno ponašanje, ne jedan red.

5.3 Ime i jedna stvar

Treće pravilo: test ne sadrži logiku. U testu nema if, petlji koje računaju očekivani rezultat, ni kopirane formule iz koda. Očekivani rezultat je konkretan broj, izračunat rukom (test orakul iz poglavlja 1). Test koji računa ocekivano = 200 + 50 * math.ceil(km - 3) samo ponavlja kod, pa ako je formula pogrešna, pogrešni su i kod i test, a test prolazi.

Česta zabluda

„Testove ne treba pisati uredno, to je samo pomoćni kod.” Testovi se čitaju češće od koda: svaki put kad padnu i svaki put kad neko želi saznati šta kod treba raditi. Dobri testovi su živa dokumentacija: iz imena testova DeliveryApp može se pročitati cijeli spisak pravila.

Ukratko – 5.3 Ime i jedna stvar

  • Ime testa kaže šta se provjerava i šta se očekuje, npr. test_dostava_besplatna_od_30_eura.
  • Jedan test provjerava jedno ponašanje; može imati više provjera ako su dio istog ponašanja.
  • Test nema logiku: očekivani rezultat je konkretan broj izračunat rukom.
  • Dobri testovi su živa dokumentacija pravila.

Provjeri se

  1. Zašto se jedinica testira izolovano od baze? Da test bude brz, ponovljiv i da pokaže tačno mjesto greške.
  2. Koja su tri dijela dobrog testa? Pripremi, izvrši, provjeri.
  3. Zašto očekivani rezultat ne smije biti izračunat istom formulom kao u kodu? Jer bi pogrešna formula dala pogrešan i kod i test, pa bi test prolazio.

Odgovori: tipka R ili klik na zamućeno polje.

Dio 2 · Sedmica 5

Dio 2 – Alati pytesta

5.4 Parametrizacija

Granice dostave traže mnogo sličnih testova: 0 km, 3 km, 3,01 km, 4 km, 4,2 km, 10 km… Osam funkcija koje se razlikuju samo u dva broja je dosadno pisati i lako je pogriješiti pri kopiranju. pytest zato ima parametrizaciju: jedan test, izvršen za svaki red tabele.

import pytest
from deliveryapp import cijena_dostave

@pytest.mark.parametrize("km, ocekivano", [
    (0, 200), (3, 200), (3.01, 250), (3.1, 250),
    (4, 250), (4.2, 300), (9.99, 550), (10, 550),
])
def test_dostava_po_km(km, ocekivano):
    assert cijena_dostave(km, 1000) == ocekivano

Oznaka @pytest.mark.parametrize dobije imena parametara ("km, ocekivano") i listu redova. pytest izvrši test osam puta, svaki put s drugim redom, i svaki red se u izvještaju vidi kao poseban test:

test_granice.py::test_dostava_po_km[0-200] PASSED
test_granice.py::test_dostava_po_km[3-200] PASSED
test_granice.py::test_dostava_po_km[3.01-250] PASSED
...

5.4 Parametrizacija

Kad padne jedan red, ostali se i dalje izvrše, i u izvještaju se tačno vidi koji red je pao. Parametrizovan test je, u stvari, tabela očekivanih rezultata iz poglavlja 1 pretvorena u kod: dodavanje novog slučaja je dodavanje jednog reda.

Ukratko – 5.4 Parametrizacija

  • Parametrizacija izvršava jedan test za svaki red tabele podataka.
  • Svaki red je u izvještaju poseban test, pa se vidi tačno koji je pao.
  • Parametrizovan test je tabela očekivanih rezultata u kodu.

5.5 Izuzeci i decimalni brojevi

Neki ulazi nisu dozvoljeni: 11 km je izvan zone (PP-3), 4,99 € je ispod minimuma (PP-1). Funkcija tada ne vraća broj, nego javlja izuzetak (raise ValueError(...)). I to je ponašanje koje treba testirati. U pytestu se koristi pytest.raises:

@pytest.mark.parametrize("km", [-0.01, 10.01, 11])
def test_izvan_zone(km):
    with pytest.raises(ValueError):
        cijena_dostave(km, 1000)

Blok with pytest.raises(ValueError): kaže: „kod unutar ovog bloka mora javiti ValueError”. Ako ga javi, test prolazi. Ako ga ne javi, test pada s porukom DID NOT RAISE (kao u 5.2). Može se provjeriti i tekst poruke, s match: pytest.raises(ValueError, match="izvan zone").

Testovi izuzetaka se često zaboravljaju, a baš oni hvataju opasne defekte: funkcija koja za 11 km tiho vrati 600 centi umjesto da odbije narudžbu šalje dostavljača izvan grada.

5.5 Izuzeci i decimalni brojevi

Zašto DeliveryApp računa u centima? U pytestu postoji i pytest.approx za poređenje decimalnih brojeva, i treba ga poznavati. Probajte u Pythonu:

>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
>>> round(2.675, 2)
2.67

Računar decimalne brojeve čuva u binarnom obliku, i većina brojeva kao 0,1 ne može se zapisati tačno, isto kao što se 1/3 ne može tačno zapisati decimalno (0,333…). Rezultat je malo pogrešan. Za fiziku je to u redu, i tada se koristi assert x == pytest.approx(0.3), koje dozvoljava sitnu razliku. Ali za novac to nije u redu: kupac ne smije platiti 24,499999 €. Zato DeliveryApp sve iznose čuva kao cijele brojeve centi (24,50 € = 2450), a decimalni zarez se pojavljuje samo pri ispisu na ekran (funkcija eur). Cijeli brojevi se u Pythonu računaju tačno, pa se testovi novca pišu s običnim ==.

5.5 Izuzeci i decimalni brojevi

Za radoznale

Python ima i modul decimal za tačan decimalni račun, koji koriste banke i knjigovodstveni programi. Cijeli centi su jednostavniji i dovoljni za DeliveryApp.

Ukratko – 5.5 Izuzeci i decimalni brojevi

  • pytest.raises provjerava da kod javlja izuzetak za nedozvoljen ulaz; ako ga ne javi, test pada s DID NOT RAISE.
  • Testovi izuzetaka hvataju opasne defekte, npr. dostavu izvan zone.
  • 0.1 + 0.2 nije tačno 0.3, jer se decimalni brojevi čuvaju binarno.
  • Zato se novac čuva u cijelim centima, a pytest.approx se koristi samo za mjerenja i fiziku.

5.6 Fixture: zajednička priprema

Testovi klase Narudzba često trebaju isto početno stanje, npr. prihvaćenu narudžbu. Umjesto da svaki test ponavlja dva reda pripreme, pytest ima fixture: funkciju koja pripremi objekat i preda ga testu.

5.6 Fixture: zajednička priprema

import pytest
from deliveryapp import Narudzba

@pytest.fixture
def prihvacena():
    n = Narudzba("N-1001")
    n.promijeni("PRIHVAĆENA")
    return n

def test_prihvacena_moze_u_pripremu(prihvacena):
    prihvacena.promijeni("U_PRIPREMI")
    assert prihvacena.stanje == "U_PRIPREMI"
    assert prihvacena.istorija == ["NOVA", "PRIHVAĆENA", "U_PRIPREMI"]

def test_prihvacena_se_moze_otkazati(prihvacena):
    assert prihvacena.moze_otkazati()

5.6 Fixture: zajednička priprema

Kako pytest zna šta da preda? Po imenu: test ima parametar prihvacena, a postoji fixture istog imena, pa ga pytest pozove i rezultat preda testu. Važno: fixture se poziva za svaki test iznova, pa svaki test dobija svoju, novu narudžbu. Kad jedan test promijeni stanje u U_PRIPREMI, drugi test i dalje dobija prihvaćenu narudžbu. To je slovo I iz FIRST (nezavisnost, poglavlje 4).

Fixture se mogu staviti u fajl conftest.py i tada ih vide svi testovi u folderu. U DeliveryApp conftest.py već bira verziju koda (ispravnu ili v0). Fixture mogu i počistiti poslije testa (npr. obrisati privremenu bazu), o čemu više u poglavlju 9.

Ukratko – 5.6 Fixture: zajednička priprema

  • Fixture je funkcija koja pripremi objekat ili stanje za test, a pytest je poveže s testom po imenu parametra.
  • Svaki test dobija novu kopiju, pa testovi ostaju nezavisni.
  • Fixture u conftest.py vide svi testovi u folderu.

Provjeri se

  1. Kako se jednim testom provjeri osam udaljenosti? Parametrizacijom: @pytest.mark.parametrize s listom od osam redova.
  2. Šta znači poruka DID NOT RAISE? Očekivali smo izuzetak, a kod ga nije javio.
  3. Zašto DeliveryApp čuva novac u centima? Jer se decimalni brojevi ne čuvaju tačno (0.1 + 0.2 ≠ 0.3), a cijeli brojevi se računaju tačno.

Odgovori: tipka R ili klik na zamućeno polje.

Dio 3 · Sedmica 5

Dio 3 – Razvoj vođen testovima

5.7 Crveno, zeleno, refaktoriši

Do sada smo testove pisali poslije koda. Razvoj vođen testovima (TDD), koji je Kent Beck opisao u knjizi iz 2002. godine, okreće redoslijed. Radi se u kratkom ciklusu od tri koraka, koji traje nekoliko minuta:

  1. Crveno. Napiši jedan mali test za ponašanje koje još ne postoji. Pokreni ga i vidi da pada (pytest ga boji crveno). Ako ne pada, test ne provjerava ništa novo.
  2. Zeleno. Napiši najmanje koda koji zadovolji test. Ne ljepše, ne opštije, samo da test prođe (zeleno).
  3. Refaktoriši. Kad su svi testovi zeleni, uredi kod: bolja imena, bez ponavljanja, konstante umjesto brojeva. Testovi čuvaju da se ponašanje ne promijeni.

Pa ispočetka, sa sljedećim malim testom.

Zašto bi neko radio ovako? Četiri koristi:

5.7 Crveno, zeleno, refaktoriši

  • Svaki red koda ima test, jer je kod nastao da zadovolji test.
  • Test se vidi da pada, pa znamo da zaista nešto provjerava (test koji nikad nije pao možda ne provjerava ništa).
  • Mali koraci: greška je uvijek u posljednjih nekoliko redova, pa se nađe za minutu.
  • Bolji dizajn: ko piše test prvi, prvo misli kako će se funkcija koristiti, pa dobija jednostavnije i testabilne funkcije.

TDD nije uvijek najbolji izbor (za istraživanje nove ideje ili za izgled ekrana manje pomaže), ali za pravila poput onih u DeliveryApp je odličan.

Slika 5.2 – Ciklus crveno–zeleno–refaktoriši
Slika 5.2 – Ciklus crveno–zeleno–refaktoriši

Ukratko – 5.7 Crveno, zeleno, refaktoriši

  • TDD radi u ciklusu: crveno (test pada), zeleno (najmanje koda da prođe), refaktoriši (uredi kod uz zelene testove).
  • Koristi: svaki red koda ima test, test se vidi da pada, mali koraci i bolji dizajn.
  • TDD je odličan za pravila, a manje pomaže kod istraživanja i izgleda ekrana.

5.8 Napojnica, korak po korak

Udruženje restorana traži novo pravilo:

PP-8. Kupac može ostaviti napojnicu dostavljaču od 0, 5, 10 ili 15 % iznosa artikala. Napojnica se zaokružuje naviše na 10 centi i iznosi najviše 10,00 €.

(Recenzija iz poglavlja 2 je pomogla: ovaj put pravilo kaže kako se zaokružuje.) Razvijamo funkciju napojnica(iznos, procenat) postupkom TDD.

Krug 1 – crveno. Najjednostavniji slučaj: bez napojnice.

5.8 Napojnica, korak po korak

from napojnica import napojnica

def test_bez_napojnice():
    assert napojnica(2150, 0) == 0

Pokrenemo: pada, jer modul napojnica ne postoji (ModuleNotFoundError, u izvještaju E). Crveno.

Krug 1 – zeleno. Najmanje koda:

def napojnica(iznos, procenat):
    return 0

5.8 Napojnica, korak po korak

Zeleno. Da, ovo izgleda smiješno, ali je namjerno: test ne traži više, pa ne pišemo više.

Krug 2 – crveno. Pravi slučaj: 10 % na N-1001. Rukom: 10 % od 21,50 € je 2,15 €, naviše na 10 centi je 2,20 €.

def test_deset_posto_n1001():
    assert napojnica(2150, 10) == 220

Pada: assert 0 == 220. Crveno.

Krug 2 – zeleno, prvi pokušaj. Napišemo return iznos * procenat // 100. Pada: assert 215 == 220. Zaboravili smo zaokruživanje na 10 centi! Test nas je upozorio u sekundi. Drugi pokušaj, sa zaokruživanjem naviše:

5.8 Napojnica, korak po korak

def napojnica(iznos, procenat):
    centi = -(-iznos * procenat // 100)   # dijeljenje zaokruženo naviše
    return -(-centi // 10) * 10            # naviše na 10 centi

Zeleno, oba testa. (Trik -(-a // b) daje dijeljenje cijelih brojeva zaokruženo naviše, bez decimalnih brojeva; // sam zaokružuje naniže.)

Krug 3 – crveno. Granica od 10,00 €: 15 % od 90,00 € je 13,50 €, a smije biti najviše 10,00 €.

@pytest.mark.parametrize("iznos, procenat, ocekivano", [
    (2000, 5, 100),      # tačno 1,00 €, nema zaokruživanja
    (2010, 5, 110),      # 1,005 € → 1,01 € → 1,10 €
    (3200, 15, 480),     # N-1002: 4,80 €
    (6660, 15, 1000),    # 9,99 € → 10,00 € (granica)
    (9000, 15, 1000),    # 13,50 € → najviše 10,00 €
])
def test_zaokruzivanje_i_granica(iznos, procenat, ocekivano):
    assert napojnica(iznos, procenat) == ocekivano

5.8 Napojnica, korak po korak

Četiri reda prolaze, posljednji pada: assert 1350 == 1000. Dodamo min(..., 1000). Zeleno.

Krug 4 – crveno. Nedozvoljen procenat (3 %, 20 %, −5 %) mora biti odbijen:

@pytest.mark.parametrize("procenat", [-5, 3, 20, 100])
def test_nedozvoljen_procenat(procenat):
    with pytest.raises(ValueError, match="0, 5, 10 ili 15"):
        napojnica(2150, procenat)

Pada: DID NOT RAISE. Dodamo provjeru. Zeleno.

Refaktorisanje. Svi testovi su zeleni, pa uređujemo: brojevi postaju konstante s imenima, dodajemo opis funkcije. Konačna verzija (primjeri/deliveryapp/napojnica.py):

5.8 Napojnica, korak po korak

DOZVOLJENI = {0, 5, 10, 15}
MAX_NAPOJNICA = 1000        # 10,00 €

def napojnica(iznos, procenat):
    """Napojnica u centima za iznos artikala u centima."""
    if procenat not in DOZVOLJENI:
        raise ValueError("napojnica može biti 0, 5, 10 ili 15 %")
    centi = -(-iznos * procenat // 100)   # dijeljenje zaokruženo naviše
    na_10 = -(-centi // 10) * 10           # naviše na 10 centi
    return min(na_10, MAX_NAPOJNICA)

Pokrenemo sve testove ponovo: 11 prolaza. Funkcija je nastala za četiri kruga, svaki red ima test, a jednu grešku (zaboravljeno zaokruživanje) test je uhvatio prije nego što je kod iko vidio.

Na času

Predavač radi TDD uživo, a studenti diktiraju sljedeći test. Pravilo igre: niko ne smije napisati red koda dok ne postoji test koji pada.

Ukratko – 5.8 Napojnica, korak po korak

  • Napojnica je razvijena u četiri kruga: bez napojnice, 10 % na N-1001, zaokruživanje i granica 10,00 €, nedozvoljen procenat.
  • Test je u drugom krugu uhvatio zaboravljeno zaokruživanje: assert 215 == 220.
  • Refaktorisanje je uvelo konstante, a 11 testova je potvrdilo da se ponašanje nije promijenilo.

5.9 Šta jedinični testovi ne vide

Jedinični testovi su temelj piramide, ali nisu cijela piramida. Oni ne vide:

  • saradnju dijelova: funkcija napojnica je ispravna, ali da li je API zaista poziva i da li ekran prikazuje rezultat? (integracioni testovi, poglavlje 9);
  • pogrešno razumijevanje pravila: ako je programer pogrešno shvatio pravilo, i test i kod su pogrešni na isti način (zato je važna recenzija, poglavlje 2, i prihvatni testovi, poglavlje 10);
  • nefunkcionalne osobine: brzinu pod opterećenjem, sigurnost, upotrebljivost;
  • okruženje: drugu verziju Pythona, drugi operativni sistem, pravu bazu.

Ponekad jedinica zavisi od nečega što u testu ne želimo pozvati: servis za plaćanje, servis mapa, trenutno vrijeme. Tada se taj dio zamijeni test dvojnikom: lažnim objektom koji glumi pravi i vraća ono što test treba. Na primjer, funkcija koja računa udaljenost preko servisa mapa u testu dobija lažni servis koji uvijek vrati 4,2 km. Test dvojnike (lažnjake, stubove i mockove) detaljno učimo u poglavlju 9.

5.9 Šta jedinični testovi ne vide

Iz stvarnog svijeta

Mnogi timovi mjere da je većina njihovih defekata „na šavovima”: između dijelova koji su svaki za sebe bili dobro testirani. Heathrow 2008. iz poglavlja 2 je veliki primjer. Zato iznad jediničnih testova uvijek treba bar nekoliko integracionih i sistemskih.

Ukratko – 5.9 Šta jedinični testovi ne vide

  • Jedinični testovi ne vide saradnju dijelova, pogrešno shvaćeno pravilo, nefunkcionalne osobine i okruženje.
  • Zavisnosti koje ne želimo zvati u testu zamjenjuju se test dvojnicima (poglavlje 9).
  • Mnogo defekata je na šavovima između dobro testiranih dijelova.

Provjeri se

  1. Koja su tri koraka TDD-a? Crveno (test pada), zeleno (najmanje koda da prođe), refaktoriši.
  2. Kolika je napojnica od 10 % na N-1001? 2,20 € (2,15 € zaokruženo naviše na 10 centi).
  3. Može li jedinični test otkriti da je programer pogrešno shvatio pravilo? Obično ne, jer su tada i test i kod pogrešni na isti način.

Odgovori: tipka R ili klik na zamućeno polje.

Praksa · Sedmica 5

Praktična nastava – TDD u DeliveryApp

Praktična nastava – TDD u DeliveryApp

Cilj je razviti dvije nove funkcije postupkom TDD i napisati jedinične testove klase Narudzba. Radi se u parovima, po modelu „ping-pong”: jedan piše test koji pada, drugi kod koji ga zadovolji, pa zamjena.

Korak 1 – Ponavljanje napojnice (30 minuta)

  1. Bez gledanja u knjigu, ponovite četiri kruga napojnice iz 5.8 u svom repozitoriju: napojnica.py i tests/test_napojnica.py.
  2. Poslije svakog kruga napravite commit s porukom „crveno: …”, „zeleno: …” ili „refaktoriši: …”. Istorija u Gitu mora pokazati ciklus.
  3. Pokrenite pytest -v tests/test_napojnica.py: mora biti 11 prolaza. Uporedite s primjeri/deliveryapp/napojnica.py.

Korak 2 – Nova funkcija: procjena vremena dostave (60 minuta)

Pravilo za TDD:

PP-9. Procijenjeno vrijeme dostave u minutama je: vrijeme pripreme restorana + 4 minute po započetom kilometru, zaokruženo naviše na 5 minuta. Vrijeme pripreme je 20 minuta za R-1, 15 za R-2 i 10 za R-3. Za nepoznat restoran funkcija javlja ValueError.

  1. Prije koda napišite tabelu od bar šest slučajeva s očekivanim vremenom, izračunatim rukom. Primjer: R-1, 4,2 km → 20 + 4 × 5 = 40 minuta → 40.
  2. Razvijte vrijeme_dostave(restoran, km) u krugovima crveno–zeleno–refaktoriši. Svaki krug jedan commit.
  3. Na kraju: parametrizovan test za sve redove tabele i test izuzetka za nepoznat restoran.
  4. Recenzija u paru: šta pravilo ne kaže? (Npr. šta je sa 0 km, i da li „započeti kilometar” znači da 3,0 km daje 3 ili 4 kilometra.) Zapišite nalaze kao u poglavlju 2.

Korak 3 – Klasa Narudzba (45 minuta)

  1. Napravite tests/test_moja_narudzba.py s fixtureom prihvacena (5.6) i još jednim fixtureom na_putu (narudžba prošla NOVA → PRIHVAĆENA → U_PRIPREMI → NA_PUTU).
  2. Napišite testove po obrascu pripremi–izvrši–provjeri: bar dva dozvoljena prelaza, bar tri nedozvoljena (s pytest.raises) i test istorije.
  3. Pokrenite nad obje verzije (DeliveryApp=v0). Koji test hvata D-04?

Korak 4 – Decimalni brojevi (15 minuta)

U Python konzoli probajte 0.1 + 0.2, round(2.675, 2) i sum([0.1] * 10). Zapišite rezultate. Zatim napišite funkciju ukupno_eur(stavke) koja sabira cijene u eurima kao decimalne brojeve (9.5, 2.5, 0.1 …) i nađite listu stavki za koju zbir nije tačan. Objasnite zašto DeliveryApp koristi cente.

Korak 5 – AI kao par (opcionalno)

Zatražite od AI asistenta da napiše funkciju vrijeme_dostave i testove za nju odjednom. Pokrenite te testove nad vašom implementacijom i vaše testove nad AI implementacijom. Da li se slažu? Ako ne, ko je u pravu po pravilu PP-9? Zapišite primjer u kojem je AI napisao test koji samo ponavlja svoju formulu (logika u testu, 5.3).

Rezultat koji se predaje: repozitorij s napojnica.py, vrijeme_dostave.py i testovima, istorija commitova koja pokazuje cikluse TDD-a, tests/test_moja_narudzba.py s fixtureima, i fajl rezultati-5.md s tabelom PP-9, nalazima recenzije, rezultatima nad v0 i primjerima iz koraka 4.

Vježbe

Vježbe za studenta

Rješenja su sakrivena – tipka R ili klik na okvir.

Vježba za studenta 1 – pripremi, izvrši, provjeri.

Zadatak

Označite tri dijela u testu i recite šta nije u redu:

def test_narudzba():
    n = Narudzba("N-1005")
    n.promijeni("PRIHVAĆENA")
    assert n.stanje == "PRIHVAĆENA"
    n.promijeni("U_PRIPREMI")
    assert n.stanje == "U_PRIPREMI"
    n.promijeni("NA_PUTU")
    n.promijeni("ISPORUČENA")
    assert n.istorija[-1] == "ISPORUČENA"

Vježba za studenta 1 – pripremi, izvrši, provjeri. – rješenje

Rješenje (R)

Pripremi je prvi red; ostatak je naizmjenično izvrši i provjeri. Test provjerava četiri prelaza u jednom, pa kad padne, ne zna se koji prelaz je pokvaren, a ime test_narudzba ne kaže ništa. Bolje: odvojeni testovi s imenima kao test_nova_moze_biti_prihvacena, ili jedan test test_puni_put_do_isporuke koji provjerava cijelu istoriju na kraju.

Vježba za studenta 2 – parametrizacija.

Zadatak

Napišite parametrizovan test za popust: 21,50 € → 2,15 €; 49,90 € → 4,99 €; 50,00 € → 5,00 €; 80,00 € → 5,00 €; nepoznat kod STUDENT20 na 21,50 € → 0,00 €.

Vježba za studenta 2 – parametrizacija. – rješenje

Rješenje (R)
@pytest.mark.parametrize("iznos, kod, ocekivano", [
    (2150, "STUDENT10", 215), (4990, "STUDENT10", 499), (5000, "STUDENT10", 500),
    (8000, "STUDENT10", 500), (2150, "STUDENT20", 0),
])
def test_popust(iznos, kod, ocekivano):
    assert popust(iznos, kod) == ocekivano

Vježba za studenta 3 – izuzetak.

Zadatak

Napišite test koji provjerava da je narudžba od 4,99 € na 2 km odbijena s porukom koja sadrži riječ „minimalne”.

Vježba za studenta 3 – izuzetak. – rješenje

Rješenje (R)
def test_ispod_minimuma_odbijeno():
    with pytest.raises(ValueError, match="minimalne"):
        cijena_dostave(2, 499)

(Poruka u kodu je „iznos manji od minimalne narudžbe”.)

Vježba za studenta 4 – napojnica rukom.

Zadatak

Izračunajte napojnicu po PP-8: a) 5 % na 24,50 €; b) 15 % na 32,00 €; c) 10 % na 99,99 €; d) 15 % na 70,00 €.

Vježba za studenta 4 – napojnica rukom. – rješenje

Rješenje (R)

a) 1,225 € → 123 centa → naviše na 10: 1,30 €. b) 4,80 € → 4,80 €. c) 9,999 € → 1000 centi → 10,00 €. d) 10,50 € → najviše 10,00 €.

Vježba za studenta 5 – TDD redoslijed.

Zadatak

Student je napisao cijelu funkciju vrijeme_dostave, pa onda pet testova, i svi su odmah prošli. Je li to TDD? Šta je rizik?

Vježba za studenta 5 – TDD redoslijed. – rješenje

Rješenje (R)

Nije: testovi su napisani poslije koda i nijedan nije viđen da pada. Rizik je da neki test ne provjerava ono što misli (npr. pogrešan parametar, ili očekivani rezultat prepisan iz koda), a mi to ne znamo, jer test koji nikad nije pao možda ne provjerava ništa. Brza provjera: namjerno pokvariti kod i vidjeti da li testovi padnu (ideja testiranja mutacijama, poglavlje 7).

Sažetak u pet rečenica

  1. JUnit, prvi veliki alat porodice xUnit, nastao je 1997. u avionu prema Atlanti, napisan „test prije koda”, i pokazuje koliko je jednostavna ideja jediničnog testa.
  2. Jedinični test provjerava jednu funkciju ili klasu izolovano, pa je brz, ponovljiv i pokazuje tačno mjesto greške, a ima tri dijela: pripremi, izvrši, provjeri.
  3. Dobar test ima ime koje kaže šta provjerava, provjerava jedno ponašanje i nema logike, a očekivani rezultat je konkretan broj izračunat rukom.
  4. Parametrizacija, pytest.raises i fixture štede posao, a novac se čuva u cijelim centima jer 0.1 + 0.2 u računaru nije tačno 0.3.
  5. TDD radi u ciklusu crveno–zeleno–refaktoriši, njime je za četiri kruga nastala napojnica s 11 testova, a jedinični testovi ne vide saradnju dijelova ni pogrešno shvaćeno pravilo.

Pitanja za ponavljanje

  1. Kako je nastao JUnit i šta je porodica xUnit?
  2. Šta je jedinica i zašto se jedinični test izvodi izolovano?
  3. Šta znači da je kod testabilan? Šta govori kod koji je teško jedinično testirati?
  4. Objasnite obrazac pripremi–izvrši–provjeri na primjeru.
  5. Kakvo ime treba imati test? Dajte loš i dobar primjer.
  6. Zašto test ne smije sadržavati logiku za računanje očekivanog rezultata?
  7. Šta je parametrizacija i kako izgleda u pytestu?
  8. Kako se testira da funkcija javlja izuzetak? Šta znači DID NOT RAISE?
  9. Zašto 0.1 + 0.2 nije jednako 0.3 i kako DeliveryApp rješava taj problem?
  10. Šta je fixture i zašto svaki test dobija novu kopiju?
  11. Opišite ciklus crveno–zeleno–refaktoriši.
  12. Koje su koristi TDD-a?
  13. Kako je TDD pomogao kod napojnice? Kolika je napojnica od 10 % na N-1001?
  14. Šta jedinični testovi ne mogu otkriti?