05.10.2021. ·
11 min

Testiranje javascript aplikacija

Testiranje javascript aplikacija

Ono što bih hteo da obradim u ovom članku je jedna celina sa težištem na konceptualnom osnovu testiranja javascript aplikacija. Mnogi programeri umeju da pišu kod, da izvrše zadatke, ali jednostavno, kada dođe do pisanja testova - učine sve kako bi to izbegli, ili možda još gore, napišu testove mehanički, čisto da se zavara mehanizam koji meri pokrivenost testovima. 

Ovde bih hteo bih da predočim, ne toliko razloge za pisanje testova (mada ću se sporadično dotaći i toga), već način razmišljanja koji će učiniti ovaj proces lakšim i svrsishodnijim. Naravno da je TDD kao koncept notorno težak za usvojiti, zahteva disciplinu i dobro poznavanje testnih biblioteka kako ne bismo, kad već pišemo dodatni kod, “izgubili osnovnu nit” i protraćili dragoceno vreme na tehnikalije. Ovde ne želim da pišem hvalospeve TDD-u, internet je toga pun - cilj će biti da obuhvatim pristup (možda) već napisanim komponentama, pošto će ovo nesumnjivo biti češći scenario sa kojim se susrećete.

Obrazložiću osnovne tipove testova, piramidu testova koju oni sačinjavaju, različite pristupe pisanju testova za gotove komponente i neke generalne smernice.

Osnovni tipovi testova

Da bismo lakše pokrili naš projekat testovima potrebno je da ovladamo stručnim žargonom. Vladanje stručnim izrazima je naizgled nebitno (“pa šta, ako radi - radi”), ali je ključno ako želimo jasno da komuniciramo sa našim kolegama u timu. U slučaju pisanja testova, poznavanje žargona će pomoći i sa praktične strane, dozvoliće nam da se koncentrišemo na određene ciljeve u toku pisanja testa i predvidimo kako ćemo pokriti ostalu funkcionalnost u ostatku test ekosistema koji gradimo. Osnovni tipovi testova su jedinični testovi (unit), zatim integracioni testovi, i E2E (end-to-end odnosno, s jednog “kraja” aplikacije do drugog).

Jedinični testovi

Jedinični, poznati kao Unit testovi, su verovatno najčešće zloupotrebljen termin. Vrlo je lako definisati jedinicu koda kao jedan fajl i nazvati testove vezane za taj fajl jediničnim testovima. Međutim, ovo u praksi nije slučaj. Da bismo pravilno pristupili ovom tipu testa, i očekivanju da ovih testova ima najviše (više o ovome u odeljku o piramidi testova), moramo ih bliže definisati. U React svetu, jedna komponenta već sama po sebi nosi svoju “biznis” logiku. Prema tome, proveravanje da li se neka “nuspojava” (useEffect na primer) ispoljava na očekivani način spada u teritoriju integracionih testova (ovo je sledeća grupa o kojoj ću pričati). Dakle, jedinični testovi su zapravo manji od toga. Oni nas teraju da pišemo drugačiji kod (što nas čini boljim programerima, i čini naš kod boljim). Da bismo izdvojili logiku u jedinični test: 

  • prepoznajemo delove koda koji mogu biti izdvojeni kao uvezene (importovane), čiste funkcije 
  • tražimo “čiste” funkcije - funkcije koje za isti unos, daju uvek iste rezultate, baš zato što je to tip funkcije koji je ubedljivo najlakše testirati 
  • Izolujemo logiku u slojeve, odvajamo implementaciju od tačke primene da ne bismo morali da menjamo baš sve testove kada promenimo tu implementaciju (a i lakše izdvajamo male celine idealne za unit testiranje). 

To su samo neki od razloga/posledica zbog kojih je dobro pisati jedinične testove. Njihova izolovanost nam daje detaljne povratne informacije i čini kod otpornijim na refaktorisanje, a održavanje ovih testova se kasnije svodi ili na brisanje nepotrebnog ili na precizne male izmene.

testiran kod iz proverenih biblioteka

Integracioni testovi

Kada govorimo o “izolovanosti” testova, izolovani testovi se bave što manjim delom logike da bi mogli sa što većom sigurnošću da ispitaju neku funkcionalnost. Nažalost, ovako izolovana funkcionalnost često nije nešto što krajnji korisnik vidi i to umanjuje vrednost naših malih, izolovanih unit testova. Krajnjeg korisnika (čije iskustvo nam je na kraju dana najbitnije), više dotiče funkcionalnost pokrivena integracionim testovima. Skoro savršena kopija korisničkog iskustva se postiže tek E2E testovima, ali za sada, ostanimo na integracionim. Integracioni testovi testiraju kako više jedinica koda radi zajedno da bi se ostvarila neka funkcionalnost. Kada smo pokrili sekciju koda jediničnim testovima, možemo da verujemo da će njihovo izvršenje biti tačno, tada se okrećemo drugoj taktici. Pažljivim upitima testiramo da li se naša funkcionalnost odražava na korisnički interfejs. Testne biblioteke će uglavnom na neki način simulirati prikaz naših komponenata - ovo nikako nije isto što i pokretanje nekog pretraživača, ali je dovoljno bliska aproksimacija korisničkog iskustva da možemo da joj verujemo. Dakle kada testiram komponentu, ne tražim prvenstveno da li je neki servis pozvan, to bi bio školski primer testiranja implementacije, ono što želim je da vidim, kako se simulacija nekog ponašanja odrazila na korisnički interfejs. Ponavljam, ne da li je tekst koji javlja grešku crven, nego da li je prisutan, budući da nijansa crvene može da se promeni što bi učinilo da naš test padne, iako do greške u logici nije došlo. Dakle, da rezimiram šta imamo do sad: 

  • Zadatak će u praksi često biti vezan za jednu komponentu što čini kod te komponente i njenih vezanih testova našom odgovornošću
  • Predlažem kao prvi korak, zbog detaljne povratne informacije, izolovati i istestirati šta god je mogućno da se bezbolno izdvoji
  • Kada verujemo jediničnim testovima i znamo da delići koda rade u izolaciji, možemo da pređemo na integraciju
  • Integracioni test ne testira implementaciju, on ne testira da li je neki tekst crven, ili da li je neki servis pozvan
  • Integracioni test traži pouzdane znake u korisničkom interfejsu (simulaciji UI-a koju dobijamo putem testne biblioteke) da je funkcionalnost ispravna
  • Detalji implementacije se, ponavljam, NE testiraju. Ne testiram da li je tekst za prikazanu grešku crven, ili 14px, testiram da li se pojavljuje nakon simulacije neuspešnog poziva nekom APIju (na primer)

Još nešto što bih dodao u vezi sa jediničnim i integracionim testovima. Govorim o izolaciji. Izolacija se postiže postavljanjem lažnih eksternih podataka oko dela koda koji testiramo. Da bismo postigli izolaciju i izoštrili fokus kada testiramo deo koda, postavljamo lažne odgovore raznih eksternih servisa. Neću ulaziti u detalje ovoga, smatram da jest biblioteka (kao najčešće upotrebljavana biblioteka za testiranje javaskripta) ima vrlo dobro napisanu dokumentaciju koju je lako pratiti. Poenta je da, kada znamo koji test pišemo, i gde želimo da ga smestimo na testnoj piramidi, možemo da ocenimo koliki nivo izolacije (na uštrb vrednosti) je idealno napraviti. Dakle ako testiram komponentu, a servis koji komponenta poziva je već zasebno pokriven testovima, ja tada biram da postavim svoj test komponente tako, da koristi lažnu kopiju servisa koji uvek vraća nama idealan rezultat. Time dobijamo lakše održive, svrsishodnije testove, a ne, kao što se nažalost viđa u praksi, testove koji pokušavaju da pokriju sve moguće aspekte tog komponenta i koji padaju pri svakoj promeni implementacije, iako krajnja funkcionalnost ostaje netaknuta. 

kod čija je ispravnost potvrđena integracionim testovima

E2E testovi

Još jedan tip testova o kome bih još nešto rekao su E2E testovi. E2E testovi web aplikacija, po definiciji moraju da pokrenu svoju instancu čitača (Chrome, Firefox ili bilo koji drugi), trebalo bi da koriste svoju bazu podataka nalik produkcijskoj bazi podataka i svode izolovanost na minimum. Ovo znači da se služe sa što manje lažnih povrata, i da što preciznije kopiraju korisnika, odnosno korisničko iskustvo. Oni su na neki način, ekstremni integracioni testovi. E2E testovi se takođe uglavnom pišu u bibliotekama koje ili ne podržavaju ili definitivno nisu idealne za pisanje jediničnih i integracionih testova. E2E testove ponekad pišu QA inženjeri - oni zahtevaju najviše vremena i pažnje da se postave i zato ih po pravilu jedinični, a i integracioni testovi brojčano nadmašuju. Integracion i jedinični testovi doduše nemaju tu vrednost E2E testova - izolovanost smanjuje vrednost iz perspektive krajnjeg korisnika (izolovani servis može perfektno da radi, ali greška u integraciji unutar komponenta može da spreči podatke da dođu do korisnika). Jedna od popularnijih biblioteka za ovaj tip testa je cypress.io - na zvaničnoj stranici možete naći više informacija o njoj, sama sintaksa je prilično laka za ovladati.

Piramida testova

Piramida testova

Piramida testova je pejzaž koji popunjavamo našim tipovima testova. Biti svestan gde u piramidi želimo da smestimo naš test pomoći će nam da formulišemo testove koji imaju jasan cilj i koji su svesni svoje okoline. Testovi ne bi trebalo da se preklapaju (iako je ovo skoro pa neizbežno). Preklopljeni testovi znače da izmena koda na jednom mestu povlači za sobom popravku testova na više mesta. Testovi su najbolji alat za dokumentaciju, i kasnije refaktorisanje postojeće logike, i sa time na umu treba da su pisani. Dakle sam alat kojim nešto postižem, ne sme da bude težak za korištenje (u korištenje se ubraja tumačenje, popravka i nadogradnja koda), inače ga neću koristiti. Zato je bitno pisati usvrhovljene, sažete testove koji su “svesni” ostatka koda i njegove pokrivenosti. 

Bazu piramide čine jedinični testovi, oni su najlakši za pisanje i održavanje i treba da su najbrojniji. Izvršavaju se brzo i prilično su izolovani od ostatka koda - ovo se postiže lažiranjem spoljašnjih poziva i okolnih komponenata (mocking i stubing), i nažalost, umanjuje vrednost testa u odnosu na celinu naše aplikacije. Dakle, jedinični test sa puno lažiranih vrednosti precizno testira jedinicu koda, i ima vrednost za programera (daje mu sigurnost i podstiče bolje razmišljanje i kodiranje), ali rezultati jediničnih testova se često ne odražavaju značajno na krajnji rezultat koji je prikazan korisniku.

Testovi kojih je proporcionalno manje, su integracioni testovi. Oni su (idealno) građeni na dobroj bazi jediničnih testova, i bave se rezultatima interakcije između više komponenata. Dakle, tu se već komponuje niz jedinica koji rade zajedno, lažiranje se svodi na potreban minimum i najčešće pokušavamo da testiramo krajnji rezultat ove integracije koji je vidljiv u korisničkom interfejsu. Iako su teži za pisanje i održavanje, iako im je vreme izvršavanja duže što čini povratnu informaciju sporijom, ovi testovi su “vredniji” jer osiguravaju da naša aplikacija funkcioniše iz perspektive krajnjeg korisnika, što je na kraju dana i najvažnije.

Na vrhu piramide se nalaze E2E testovi. Oni su tu jer su ubedljivo najvredniji, često testirajući sve slojeve aplikacije i to iz perspektive korisnika. Oni su najsporiji za izvršavanje jer su najkompleksniji i zbog ovoga se uglavnom neće izvršavati u istom dahu sa jediničnim i integracionim testovima. Postoje granični slučajevi gde se neke stvari mogu potvrditi skoro isključivo E2E testom budući da dodavati jedinične i integracione testove na popularne biblioteke koje smo uključili, samo po sebi nema smisla.

Različiti pristupi pisanju testova

Sada kada smo upoznati sa tipovima testova koji sačinjavaju piramidu testova možemo pričati o dva popularna pristupa pisanju testova. “Od gore” i “od dole”. Naravno da se ovi smerovi odnose na sada već nama poznatu piramidu testova. 

Pristup od dole je za mene bolja opcija jer znači da počinjemo, ako ne sa TDD, onda makar sa ranom implementacijom testova. Gradimo integracione na jakoj bazi jediničnih testova. Povratna informacija je granularna i lakše možemo da uočimo gde je tačno došlo do greške. Takođe, svaka jedinica dolazi sa svojevrsnom dokumentacijom u vidu napisanih testova.

Pristup od gore je bolja opcija ukoliko smo ograničeni sa vremenom i možda planiramo uskoro da refaktorišemo kod. U tom slučaju će jedinični testovi ubrzo postati beskorisni jer će biti drastično menjani, a nas zanima samo krajnji rezultat. Tada treba da, shodno glavnim funkcijama aplikacije, pokrijemo česte scenarije i verujemo da se slojevi obuhvaćeni integracionim testovima ponašaju tačno budući da je krajnji rezultat tačan. Ovde preti opasnost da ostatak logike ne pokrijemo jediničnim testovima budući da su “važne” stvari već pokrivene.

Zaključak i opšte smernice

Testovi aplikacionog koda o kojima ovde govorim nisu testovi opterećenja. Kod aplikacije se pokriva testovima kao garancija da se pojedini procesi odvijaju kako smo planirali. To dozvoljava bezbedniju smenu programera na zadacima budući da svaki sledeći programer može da vidi rezultate svoje promene u odnosu na ključna ponašanja aplikacionog koda. Izuzetno je važno da testovi nemaju nikakve nasumične elemente. Svaki test mora da bude takav da može da se izvrši bezbroj puta, odvojeno, ili u nizu sa drugim testovima i da uvek daje istu povratnu informaciju. I za kraj, kod testova je bitno da ih pišete kao da pišete dokumentaciju. Uvek mora biti jasno šta test izvršava i u kom scenariju - zato propagiram “given-when-then” strukturu testnih jedinica. Iz prve ruke mogu da posvedočim ogromnu razliku u kvalitetu baze koda gde je testiranje barijera za pripajanje koda “glavnim” granama. Testiranje je neverovatno opširna tema, i nadam se da je ovaj “uvod” koristan mnogima koji tek počinju da pišu testove, ili ih već pišu ali nisu zadovoljni postignutim. Takođe, kao još jedna napomena, testiranje samo po sebi nije strogo standardizovano, ne isključujem da će za nekoliko godina neke paradigme da se promene, zato je debata osnova napretka, a testiranje područje gde programiranje često prelazi iz zanata u filozofiju. Jedino obećanje koje mogu da ostavim ljudima koji tek počinju da testiraju svoj javaskript kod je da nakon jednog dobro pokrivenog projekta, neće više planirati projekte bez ispunjene piramide testova na umu.

Oceni tekst

11 osoba je glasalo, prosečna ocena: 5
Siniša Nimčević Siniša Nimčević

Nakon frilans rada u zlatno doba Elance platforme i putešestvija (i radnog iskustva) po Londonu, vraća se u rodnu Suboticu leta gospodnjeg 2018. Sa oko 10 godina iskustva u vebu, specijalizuje se u javaskriptu i piskara ne bi li dublje istražio neke teme i preneo znanje. Uvek otvoren za ćaskanje, savete i konstruktivnu kritiku.

0 komentara

Iz ove kategorije

Svi članci sa Bloga