Címkék: #Adatgyár (Factory) #Dusk #Érvényesítés (Validation) #FeatureTest #Laravel #PHPUnit #Tesztelés (Testing) #UnitTest #Űrlap (Form)
Mivel már régebben foglalkoztam teszteléssel, ezért érdemes lehet feleleveníteni a teszteléssel kapcsolatos bejegyzéseimet (aki még nem tette volna meg, érdemes lehet ezen bejegyzés olvasása előtt végigfutni az alábbi bejegyzéseket, főleg a 2-3. pontban említetteket):
A
mostani bejegyzésben a validációs fejezetet szeretném lezárni, és olyan
automatikus teszteket létrehozni, amelyek utána mindig biztosítják, hogy
a már megvalósított érvényesítési (validációs) eljárásaink továbbra is
jól működnek az űrlapjainknál. Így az alkalmazásunk minősége megmaradhat olyannak, amilyet mi elterveztünk és megvalósítottunk neki.
Szoftvertechnológiai szempontból a tesztelésnek mindig kiemelt szerepe van a szoftver életciklusa során: a különböző fázisokat külön-külön és egészében is tesztelhetjük, illetve elvárás is, hogy teszteljük azokat. Mi most az űrlapjaink kitöltésének ellenőrzésére fogunk koncentrálni.
Informatikusként, programozóként, adatbázis tervezőként, adatelemzőként, ... picit nehéznek tűnhet teszteseteket megtervezni és írni, mivel ez is egy művészet, külön rá kell érezni és egy külön szakma épül erre, különböző vizsgázási lehetőségekkel is. Én most a tesztelés teljes mélységébe nem mennék bele, de itt van egy kiváló dokumentum, amit szívesen ajánlok, mert elmagyarázza a tesztelés alapjait, technikáit, menedzselését stb. Ha pedig ennél is mélyebben érdeklődik valaki a téma iránt, akkor feltétlenül olvassa el Robert C. Martin - Tiszta kód című alapvetését, amit amúgy minden szoftverfejlesztőnek ajánlok.
De nem szeretnék túlságosan elkanyarodni a témánktól és próbálom tartani a fókuszt a validáció tesztelésén. Nézzük meg inkább a folyamatot, amit szeretnék tesztelni:
Érdemes felmérni az űrlapon leggyakrabban előforduló bemenettípusokat, érvényes és érvénytelen bemeneteket azért, hogy tisztában legyünk vele, milyen módokon is lehet majd tesztelni őket. Azért is fontos ezeket már ilyenkor megcsinálni, mert ha véletlenül valamit a szoftverfejlesztő rosszul implementált vagy elnézett valamilyen kombinációt, amely bekövetkezése miatt a felhasználói elfogadási teszt (User Acceptance Testing) vagy a rendszerteszt (System Testing) során hibát kapunk, akkor az nagyon "ciki" tud lenni, úgyhogy jobb elkerülni az ilyen szituációkat.
Bemenet típusok | Érvényes bemenetek | Érvénytelen bemenetek |
Numerikus érték (egész vagy lebegőpontos szám) |
|
|
Szöveg (karakter vagy karakterlánc) |
|
|
Dátum és/vagy idő |
|
|
Bemenet típus lehet még a listából (select options, radio buttons, checkbox lists) választás, de itt általában elég megkövetelni azt, hogy kötelező-e kiválasztani valamit vagy sem.
Dinamikus tesztek tervezéséhez a következőket hajtsuk végre a tesztesetek kapcsán:
Mielőtt még a validációs szabályok ellenőrzésébe belevágnánk, tekintsük át, hogy jelenleg milyen tesztjeink, teszteseteink vannak, futtassuk az utasítást:
php artisan test
Amivel a következő eredményt kapjuk:
Mindegyik tesztünk pozitívan lefutott, zöldek vagyunk, örülünk. Ezek a tesztek a tests / Unit és a tests / Feature
almappákban vannak és ha megnyitjuk, végiggörgetjük őket, akkor
láthatjuk, hogy ezek főleg útvonal példák vagy adatbázis gyárral
(factory) kapcsolatos tesztesetek voltak.
De, ha egy pillanatra megállunk és felidézzük Laravel Dusk-os emlékeinket, akkor megnézhetjük a tests / Browser mappát is, amiben ott van szintén egy példa, de van nekünk egy utasokat létrehozó tesztesetünk is, a PassengerTest osztály.
Tipp: mindenekelőtt, ha még nem tettük volna meg, indítsuk el az alkalmazásunk kiszolgálását (php artisan serve), majd mielőtt belevágnánk a további tesztelgetésbe, és főleg, ha már régebben használtuk a Laravel Dusk-ot a Chrome böngészőben, akkor érdemes frissíteni a driver-ét: php artisan dusk:chrome-driver
Futtassuk is a php artisan dusk tests/Browser/PassengerTest.php
parancsot, aminek hatására megnyílik a Chrome böngészőnk, az utasokat
létrehozó űrlappal. Az utas neve kitöltődik és a repülőjárata
kiválasztódik, viszont a többi (azóta hozzáadott bemeneti elem piros
keretű marad) és az adatokat nem lehet elküldeni a Mentés gomb
"megnyomásával". (Emlékezzünk! Az iménti utasítás
hatására a tesztelésre használt üres adatbázisunkat - laravel_v9_dusk -
fogja használni a rendszer és feltölteni a flights adattáblánkat azzal a
3 repülőjárattal, ami ennek a tesztesetnek az elején kerül definiálásra
és létrehozásra.) A lefutás egy részlete itt látható:
A böngésző jó gyorsan be is záródik és megkapjuk a visszajelzést a hibáról a terminal-ban:
Sárgával kiemeltem azokat a részeket, amik számunkra most fontosak. Tehát van 1 hibánk, méghozzá a a_visitor_can_create_a_passenger függvényen belül, méghozzá az, hogy a Mentés gomb nem kattintható, írja is, hogy a 24. sorban van a kattintás meghívása nálam.
És miért nem volt kattintható? Pontosan azért, mert ennyi adat kitöltése nem felelt meg a teljes űrlap validációjának. Javítás: ez inkább a --headless Dusk beállítás miatt nem volt kattintható, a magyarázatot lásd később.
Én mindig azt mondom (kevésbé "TDD-látásmóddal"), hogy először hozzunk össze egy működő funkcionalitást, utána végezzük el a finomhangolásokat, mint például a validáció. Lehet ezt pont fordítva is csinálni, hogy minden egyes űrlap mezőt teljesen részletesen végiggondolunk, hogy milyen probléma lehet vele és emiatt milyen validációt is alkalmazzunk nála, majd csak utána lépünk tovább a következő űrlapelemre, ha már ez az adott megvan minden szempontból.
Az iméntiek miatt tesztelési szempontból én azt javaslom, hogy először készítsünk el (illetve bővítsük ki a már meglévő) tesztesetünket olyanra, ami érvényes (valid) eredményt produkál a kliens és szerveroldali validáció szempontjából. Emlékeztetőül, ezzel az üres űrlappal foglalkozunk:
(Látható is, hogy a Mentés gombunk nem kattintható, amíg érvénytelenek a bemenetek.)
A
tesztesetünk az utas nevét és a repülőjáratát kiválasztja, viszont az
életkort, e-mail címet, telefonszámot nem, továbbá a születési dátum
alapértelmezett értéke a mai napra van állítva, ami biztosan nem lesz
jó, hiszen ennek legalább 18 évvel korábbi dátumnak kell lennie (bármit
is állít a felhasználó az életkor mező kitöltésénél). Mi, mint akik
programoztuk az alkalmazást, ismerjük a feladatunk specifikációját és ismerjük a kliens/szerver oldali validációnk működését is, emiatt fogunk tudni olyan teszteset megadni, aminek érvényes lesz a kimeneti eredménye, és olyat is, aminek érvénytelen.
Egy gondolat még kiegészítésül az úgynevezett ekvivalencia osztályokról: nekünk nem kell, például az életkor kapcsán, az összes értéket végigtesztelni 6-tól 99-ig, mert az ezek közötti számok mindannyian megfelelőek, ezért ebből a halmazból mindössze egy értéket érdemes kiválasztani, amivel az egész halmazt tudjuk reprezentálni. Továbbá pont emiatt érdemes az érvénytelen (invalid) tesztelésre egy-egy számot választani a táblázat felsorolásából (halmazaiból, ekvivalencia osztályaiból), ami mondjuk 6 alatti vagy ami 99 feletti, a jelenlegi példánkban. De sokszor a negatív számok vagy a 0 okozzák a problémát, érvénytelenséget, úgyhogy ezekre különösen figyeljünk és teszteljük őket. Ami még kimondottan problémás szokott lenni, úgyhogy ezeket szokták is tesztelni, azok a határokon lévő értékek, az életkornál például a 6 még jó érték vagy már nem?
Először tehát egy érvényes tesztesetet hozzunk létre. Ez például egy megfelelő:
$this->browse(function (Browser $browser) {
$browser->visit('/passengers/create')
->type('utasneve', 'Gludovátz Attila') // input mező name attribútum értéke
->type('age', 32)
->keys('#birthdate', '1990', '{tab}', '01', '01')
->type('email', 'attila@gludovatz.hu')
->type('phone', '20/123-4567')
->select('repulojarata')
// ->click('input[type="submit"]')
->click('@save-button')
->assertSee('Gludovátz Attila');
});
Ahogy említettem, az "utasneve" és a "repulojarata" mezők kitöltése már megvolt korábban. Az új értékeknél az "age", "email", "phone" nem okozhatott problémát, könnyű volt neki érvényes értékeket megadni. A trükkös megoldás az a date típusú input mezőnél adódott, itt több dologra is kellett figyelni:
Az alkalmazást teszteljük ezzel az utasítással, hogy színes (zöld vagy piros) legyen az eredmény:
php artisan dusk tests/Browser/PassengerTest.php --colors=always
Viszont még hibánk van... Ez így még mindig nem elég ahhoz, hogy megfelelően működjön a teszteset, mivel két további Dusk beállítás is akadályozhat minket a végső sikeres (érvényes) lefutás elérésében. Nyissuk meg a DuskTestCase.php beállításokat tartalmazó fájlt és a driver metódus $options beállításaira koncentráljunk. Kiemeltem alább azt a két sort, amivel foglalkoznunk kell:
$options = (new ChromeOptions)->addArguments(collect([
'--window-size=1920,1080',
'--lang=hu_HU',
])->unless($this->hasHeadlessDisabled(), function ($items) {
return $items->merge([
'--disable-gpu',
'--headless',
]);
})->all());
Ez volt tehát az érvényes tesztünk lefuttatása, hiszen a click('@save-button') esemény után kijutunk az utasok index oldalára és már látni is fogjuk az újonnan létrehozott utasunk nevét.
Készítsünk egy olyan tesztesetet, ami elbukik a validáció "eltörése" miatt. Ehhez persze jó, ha tisztában vagyunk azzal, hogy maga a Laravel milyen lehetőségeket biztosít az assert utasításokra, ezeket itt tudjuk böngészni.
Ha mondjuk az előző PassengerTest-ben lévő tesztesetet vesszük alapul és bármelyik bemenet kitöltését kikommentezzük, például a telefonszámét, akkor már el is fog bukni a tesztesetünk.
Itt most már a --headless kapcsoló nem tud "bezavarni az erőbe", ennek a tesztnek így tényleg el kellett bukni. De mit kellene tenni ahhoz, hogy bár a validáció nem teljesült, mi kapjunk egy kliens oldali visszajelzést...? Ez abból a szempontból nehezebb, hogy a jelenlegi alkalmazásunkban addig nem is enged kattintani a rendszer a Mentés gombra, amíg nem érvényes minden egyes űrlapelem (a telefonszám hiánya pedig érvénytelenséget eredményez, természetesen).
Úgyhogy szüntessük meg ezt a beállítást. A resources / sass / mystyle.scss fájlban kommenteljük ki a form:invalid input[type="submit"] selector-ra vonatkozó részt, majd futtassuk a terminal-ban az npm run dev parancsot.
Állítsuk vissza a validáció szempontjából sikeres tesztesetünk és a tartalmát másoljuk le egy új metódusba, majd ott "rontsuk el" ismét, mint az alábbi kitöltésnél.
Talán jogosan merülhetne fel a dolog, hogy ezt a kis tooltip-et (figyelmeztetést) látnunk kellene az oldalon, úgyhogy ezekután itt van az új metódusunk:
/** @test */
public function a_visitor_cant_create_a_passenger_without_phone_number()
{
Flight::factory()->count(1)->create();
$this->browse(function (Browser $browser) {
$browser->visit('/passengers/create')
->type('utasneve', 'Érvénytelen János')
->type('age', 22)
->keys('#birthdate', '1999', '{tab}', '12', '31')
->type('email', 'janos@invalid.hu')
// ->type('phone', '20/123-4567')
->select('repulojarata')
->click('@save-button')
->assertSee('Kérjük, töltse ki ezt a mezőt.');
});
}
Az iménti "látnunk kellene az oldalon" kifejezést az assertSee() metódussal tudjuk ellenőrizni. Ha viszont újra futtatjuk a tesztünket, akkor a következő visszajelzést kapjuk:
A Dusk keresi a megadott szöveget az oldal forrásában, azonban tényleg nincs benne, úgyhogy nem is látja így, jogos tehát a hibajelzés.
Ezekután már talán gondolhatjuk, hogy ha az oldal forrásában nincsen benne, akkor ez egy Javascript kód lesz. Az "assertSee"-s sort tehát kivehetjük a kódunkból (a végén a pontosvesszőt azért hagyjuk meg), majd folytathatjuk a Javascript kód ellenőrzésével a megvalósítást:
$message = $browser->script("return document.getElementById('phone').validationMessage")[0];
$this->assertEquals('Kérjük, töltse ki ezt a mezőt.', $message);
(Megjegyzés: a Laravel Dusk-nak nincs assertEquals metódusa, ezt a PHPUnit segítségével tudjuk használni.)
Fontos, hogy ezeket az utasításokat az előbbi kód után illesszük be, mivel az ott elért állapotban lesz már meg a phone mezőnek a validációs üzenete, amit aztán a következő sorban össze tudunk hasonlítani az általunk megadott szöveggel. Ha így futtatjuk a tesztet, akkor most már OK végeredményt kell visszakapnunk az assertEquals(...) utasításra.
Ha egy picit "túl is szeretnénk biztosítani" magunkat, akkor még a következő utasítást is beilleszthetjük az iméntiek után:
$browser->visit('/passengers')
->assertDontSee('Érvénytelen János');
Ezzel ellenőrizhetjük, hogy ha meglátogatjuk az utasok index oldalát, akkor tényleg nem történt meg a hozzáadás és nem látja (assertDontSee) a Dusk az új, érvénytelen utast. Erre persze már mondhatnánk, hogy szükségtelen megtekinteni, de itt a tanulási folyamatunk során talán nem haszontalan ezt így végiggondolni.
Ezen a ponton érdemes talán a kód teszt lefedettségéről beszélni. Ez a számszerű értékelése annak, hogy a tesztelési tevékenység mennyire alapos, milyen a minősége. A lefedettség egy mérőszám, amivel gyakorlatilag megmondhatjuk, hogy az implementált kódunkat milyen mértékben teszteltük. Például, ha csak ezt az utas létrehozási űrlapot tekintjük, akkor teszteltük az érvényes tesztesetet és az érvénytelenek közül egyet a kliens oldaliak közül, amikor hiányzott a telefonszám kitöltése. Nem teszteltük viszont még a kliens oldali tesztesetek közül azt, amikor például a név, életkor, születési dátum hiányzik, vagy például nem elég hosszú a név (legalább 10 karakteres), vagy a születési dátum nincs összhangban a megadott életkorral és így tovább... Jó sok mindent nem teszteltünk még, ezt talán érezzük, mint ahogy azt is érezhetjük, hogy minden egyes eset tesztelése elég időigényes (és valljuk meg, unalmas is lenne). Ezért a rendszerek, webalkalmazások nem is feltétlenül szokták elérni a 100%-os kód lefedettséget, hiszen ez sokkal több energiabefektetéssel (pénz+idő) járna, mint amit talán megérne. Vannak azonban biztonságkritikus rendszerek, mint például egy webes bankolási oldal, ahol a 100%-os kódlefedettség elvárt (az ügyfelek részéről is), hiszen itt még véletlenül sem történhet meg, hogy valamiről elfeledkeztek a fejlesztők. Persze ott is előfordulhat, hogy valaki hibázik, hiszen annyi mindent kellene tesztelés alá vonni... én is csak egyetlen űrlapról beszéltem eddig, miközben volna még jónéhány, amit el kellene látni teszteléssel, továbbá egyéb teszteseteket is vizsgálni kellene, például integrációs, rendszer és elfogadási teszteket is végre kellene hajtani, ügyelve mindig a biztonsági paraméterekre.
Maradjunk viszont az űrlap ellenőrzésénél, mivel nem beszéltünk még a szerver oldali validáció teszteléséről. Annyiból szerencsénk van, hogy ha az 1. példában látható érvényes teszteset átment a kliens oldali ellenőrzésen és látható az új utas az index nézet oldalán, akkor gyakorlatilag a szerver oldali validáció is sikeresen futott le. Ami problémásabb lehet, az megint az érvénytelen eshetőség, vagyis az, amikor kliens oldalon elfelejtett valamit ellenőrizni a programozó (vagy éppen megtámadták a kódját kliens oldalon és olyan adatokat küldtek el, ami átment a kliens oldali ellenőrzésen), ilyenkor van leginkább szükség a szerver oldali validációra, amit mi most tesztesettel fogunk ellenőrizni.
Előfordulhat, hogy utasokat mostantól kezdve már nem 10 karakterű névvel, hanem legalább 5 karakterű névvel lehet csak felvenni a rendszerbe (hiszen nem szeretnénk mondjuk kizárni utasaink közül Kiss Imrét, akinek rövidebb a neve, mint 10 karakter, konkrétan 9 karakteres a szóközzel együtt). Mi pedig, mint gyarló emberek, ezt csak a kliens oldali szabály frissítésénél tesszük meg, a szerver oldali ellenőrzést változatlanul hagyjuk. A nézeteknél passengers / create.blade.php-ben az "utasneve" input mező "minlength" attribútum értékét írjuk át 10-ről 5-re (és érdemes a fájl alján a Javascript kódban is a figyelmeztető üzenetben a 10-et 5-re cserélni - ha ez problémát okozna, akkor a bejegyzés végén lévő Github commit fájlhoz tartozó tartalmát érdemes megnézni).
/** @test */
public function a_visitor_cant_create_a_passenger_with_too_short_name()
{
Flight::factory()->count(1)->create();
$this->browse(function (Browser $browser) {
$browser->visit('/passengers/create')
->type('utasneve', 'Kiss Imre')
->type('age', 22)
->keys('#birthdate', '1999', '{tab}', '12', '31')
->type('email', 'imre@kiss.hu')
->type('phone', '20/123-4567')
->select('repulojarata')
->click('@save-button')
->assertSee('Kiss Imre');
});
}
Látható, hogy szépen megadunk minden helyes(nek gondolt) értéket a tesztben, viszont nem lesz sikeres a teszt lefutása, mert nem látjuk a végén "Kiss Imre"-t az utaslistában. Ez ugye a szerver oldali validáció elbukása miatt van, úgyhogy változtassuk meg az assertSee()-s sort a következőre:
->assertSee('The utasneve must be at least 10 characters.');
Ez nem túl baráti, vagy magyar, inkább olyan "Hunglish kategória", de jelenleg még nem tartalmaz a szerver oldali validációs kódunk lefordított hibaüzenetet (ha valaki ennél barátságosabb hibaüzenetet szeretne írni, akkor érdemes áttekinteni az itt megtekinthető bejegyzésemet). Viszont, ha így futtatjuk a tesztelést, akkor OK eredményt kapunk, tehát a Dusk látja a szerver oldali validációs hibaüzenetet, emiatt megfelelő is lesz a tesztünk.
Ebben a bejegyzésben a validációs témakört zártam le (egy időre) úgy, hogy teszteléssel ellenőriztük egy űrlap kitöltését és a kliens- és szerver oldali szabályok betartatását. Adtam tanácsot azoknak, akik a tesztelés témakörében el szeretnének mélyebben merülni: tanulási (szakirodalom) és vizsgázási tanácsokat is megosztottam az olvasókkal. Tesztelési elmélet szempontjából átvettük, hogy az űrlap ellenőrzésénél milyen fajta érvényes és érvénytelen bemenetek lehetnek az egyes típusoknál, de szóba kerültek az tesztelési ekvivalencia osztályok, valamint a kód teszt lefedettsége is.
Írtam arról, hogy hogyan érdemes megtervezni egy tesztesetet, majd utána ezt három részletesebb példán is végigvezettem gyakorlati szempontból bemutatva. A példák során az "apróbb" finomságokra és bosszantóbb problémákra is felhívtam a figyelmet, hogy ezáltal segítselek benneteket a tesztesetek megfelelő definiálásánál. Főként a Laravel Dusk-ot használtam, ami egy elég hatékony eszköz a validációs szabályok ellenőrzésére is.
Ahogy korábban írtam, automatikus tesztet írni, készíteni is művészet. Meg kell tanulni, de rá is kell érezni, hogy hol vannak, lehetnek a hibák. Egy rossz teszt, rossz kódot is eredményezhet, úgyhogy ilyen esetben a tesztet is felül kell vizsgálni és lehet, hogy újra meg kell írni.
A bejegyzéshez tartozó Github commit itt található.