Komplex példa - Teszteljünk! 1.0

Attila | 2022. 03. 27. 21:52 | Olvasási idő: 4 perc

Címkék: #Adatgyár (Factory) #FeatureTest #Laravel #Laravel 9 #PHPUnit #SQLite #Tesztelés (Testing) #UnitTest

A Laravel tesztelési lehetőségeivel kezdjük meg az ismerkedést gyakorlati példákon keresztül. A Laravel a PHPUnit csomagot használja arra, hogy teszteseteket lehessen futtatni a működtetésével.
feature-test-laravel

Bevezetés, alapismeretek

A Laravel könyvtárstruktúrája alapból tartalmaz egy tests nevű könyvtárat. Ebben két alkönyvtár helyezkedik el, a Feature és a Unit. A Feature-re gondoljunk úgy, mint ha egy funkcionalitást akarnánk implementálni és utána (vagy előtte) ezt szeretnénk tesztelni a projektünkben. Mondjuk az a funkcionalitásunk, hogy el akarunk adni zenéket az oldalunkon, és ezt szeretnénk végigtesztelni: (1) csak regisztrált felhasználók vásárolhatnak, (2) csak bizonyos termékeket vehetnek, (3) a fizetési folyamatot végigkövetve menedzselnénk és tesztelnénk a lépéseket stb. A UnitTest egy sokkal jobban izolált, leválasztott tesztelési lehetőséget ad nekünk az alsóbb szinteken. Mondjuk adott class adott funkcióját teszteljük ezen a szinten. Tehát a tesztelést úgy képzeljük el, mint ha kívülről befelé (outside → in) haladnánk, Feature-től indulunk majd utána a Unit teszt következik és ez úgy néz ki, hogy az adott osztály adott függvényét akarjuk tesztelni azon módon, hogy mondjuk milyen lefutási eredménnyel tér vissza… és ide-oda lépkedünk a két szint között, amíg hibákat kapjuk.

A Unit test, vagy más néven egységteszt, a metódusokat teszteli. Adott paraméterekre ismerjük a metódus visszatérési értékét (vagy mellékhatását). A Unit test megvizsgálja, hogy a tényleges visszatérési érték megegyezik-e az elvárttal: ha igen, sikeres a teszt, egyébként sikertelen. Elvárás, hogy magának a Unit test-nek ne legyen mellékhatása. A Unit test-elést minden fejlett programozási környezet (Integrated Development Environment, IDE) támogatja, azaz egyszerű ilyen teszteket írni. A jelentőségüket az adja, hogy a futtatásukat is támogatják, így egy változtatás után csak lefuttatjuk az összes Unit test-et, ezzel biztosítjuk magunkat, hogy a változás nem okozott hibát. Ezt a folyamatot nevezzük regressziós tesztnek.

Mivel a projektünk már tartalmaz is a tests könyvtáron belül (és almappáiban) ExampleTest osztályokat, ezért azok már futtathatók is. Említettem, hogy a Laravel a PHPUnit csomagot használja tesztelésre. Ez a projektünkben a vendor / bin / phpunit mappában található meg. Futtassuk is le a parancsot:

vendor/bin/phpunit

Ennek hatására láthatjuk, hogy a PHPUnit melyik verziója van telepítve és ami még fontosabb jelen esetben: a két említett osztály és annak tesztelő metódusai végre fognak hajtódni. Szerencsére zöld OK-ot kapunk vissza, tehát lefutnak hibamentesen ezek a tesztjeink (láthatjuk még a lefutás idejét és hogy mennyi memóriát használt ehhez). Azt is láthatjuk még, hogy 2 tesztünk futott le (ezt jelzi a két darab . is) és a tesztmetódusokon belül 2 "assertions". Ha hibát kaptunk volna, akkor az adott teszt helyén a . helyett piros hátterű F betűt kapnánk.

Az assert utasítás segítségével fogalmazhatunk meg programunk egy pontján egy olyan logikai állítást, amelynek igaznak kell(ene) lennie. Az assert utasítások a szemantikát és a teljesítményt illetően is az üres utasítással egyenértékűek. Ha az állítás valóban igaz, az assert utasítás hatástalan, ellenkező esetben egy Assertion hiba következik be. A Laravel 9-ben elérhető assert utasítások itt érhetők el.

Mivel most sokszor fogjuk használni a tesztelést, érdemes létrehozni egy alias nevet, amivel a fenti hosszú parancsot tudjuk meghívni csak rövidebben (Powershell-es vagy röviden, PS-es terminal-ban tudjuk kiadni ezt):

Set-Alias -Name phpunit -Value vendor/bin/phpunit

Utána már csak simán a phpunit parancs kiadásával tudjuk indítani a tesztelést. Példának okáért rögtön teszteljük ezt úgy, hogy csak egy fájlt futtatunk le:

phpunit tests/Unit/ExampleTest.php

Látható, hogy ez működik és így már csak 1 teszt és 1 assert futott le, helyesen. Ha megnézzük a 2 ExampleTest osztályt és benne a metódusokat, akkor láthatjuk, hogy a Unit mappában lévő egy sima igaz-igaz állítás egyezőségét vizsgálja meg, a Feature mappában lévő pedig lekéri a kezdőoldalunkat és megvizsgálja, hogy a visszakapott válasz az 200-as HTTP állapotkódot ad-e vissza, vagyis, hogy hibamentesen betölti-e a kezdőoldalt.

Mikor teszteljünk? Mivel most a komplex példánk már elég jól felépült, működnek a kódjaink, ezt tudjuk, ezért azt mondhatjuk, hogy az implementálás után tesztelünk éppen. De előfordulhat most is (illetve a későbbiekben bármikor) olyan funkcionalitás, amelynek előbb a tesztjét írjuk meg, aztán pedig elvégezzük a tényleges megvalósítást, és kódolunk addig, ameddig ez az új tesztünk OK eredményt nem ad vissza. Ez utóbbit hívják tesztvezérelt szoftverfejlesztésnek (test-driven development).


Saját teszt létrehozása és a szabályok

Hozzunk létre egy új tesztet és közben ismerkedjünk a tesztelés szabályaival.

php artisan make:test RoutesTest

Alapértelmezetten ilyenkor a Feature mappába fog bekerülni az új teszt osztályunk. Ha Unit tesztet hoznánk létre akkor írjuk utána a -u kapcsolót. A következő lépés, hogy hozzunk létre tesztelő metódusokat, amelyek visszajelzést adnak a webalkalmazásunk helyes működéséről. Tesztelő metódust kétféle módon hozhatunk létre:

  1. Helyezzük el a következő annotációs megjegyzést a metódus előtti sorban: /** @test */
  2. Kezdjük a metódusunk nevét így: test

Mindkettő jó megoldás, azt használjuk, ami számunkra szimpatikusabb, logikusabb, átláthatóbb. Adjunk viszont a metódusunknak nagyon beszédes, olvasható nevet, mert a teszt futtatása során, főleg amikor már nagyon sok lesz belőlük, akkor sokat tud segíteni, hogy rögtön átlássuk, mégis mivel van probléma.

Ha valaki ismeri a User Story (Felhasználói történet) tervezői eszközt, annak ismerős lehet az a stratégia, hogy hogyan építsünk fel egy ilyen tesztelési metódust (kövessük tehát a Given-When-Then, vagyis a GWT sablont):

  • Van az adott felhasználó, aki be van jelentkezve a rendszerbe;
  • Amikor a felhasználó eléri az adott útvonalat (például: /teams);
  • Akkor listázza ki neki a csapatokat.

Közben persze mindent ellenőrizzünk le assert utasításokkal. De még mielőtt túlságosan bonyolult "Feature"-ökbe, programfunkciókba mennénk bele. Próbáljunk tesztelni néhány egyszerűbb metódust és közben gyakoroljunk.

public function test_flights_page()
{
  $response = $this->get('/flights');

  $response->assertStatus(200);
}

Itt például, ha a teszt során lekérjük a /flights útvonalat, akkor elvárás, hogy a 200-as HTTP állapotkóddal térjen vissza. Vagy itt egy másik metódus:

public function test_welcome_page()
{
  $response = $this->get('/');

  $response->assertViewIs('welcome');

  $response->assertSee('Home');
}

A teszt során itt lekérjük a kezdőoldal útvonalat és megvizsgáljuk, hogy az a welcome nézet-e, illetve, hogy a visszakapott oldal HTML kódjában megtalálható-e a 'Home' felirat... nálam természetesen mindkét felvetésre igen a választ, ezt várom tehát el az oldalam működésétől. Ha valamelyik teszt hamisat ad vissza, tehát elbukik (a szakzsargon szerint), akkor a teszt nem fut le és hibát ad vissza. A hiba esetén a rendszer azt is megmondja, hogy hol és mivel volt a probléma.

Hozzunk létre egy új tesztet, ami már egy picit bonyolultabb lesz:

php artisan make:test FlightsTest

Ebben a fájlban, ahogy a neve is mondja, a repülőjáratokat szeretném majd tesztelni, de úgy, hogy a már meglévő adataimat ne "szemeteljem tele". (Megjegyzés, nekem sajnos sikerült teleszemetelnem, aztán törölni is a meglévő adataimat, úgyhogy ne essetek ugyanabba a hibába, mint én estem... már rögtön itt az elején hozzuk létre a teszteléshez használt környezetet.)

Hozzunk létre egy új környezeti fájlt a Terminal segítségével:

cp .env .env.testing

A teszteléshez egy SQLite adatbázist fogunk használni (ha nem akarunk SQLite-ot, hanem megtartanánk a MySQL-es adatkapcsolatot, akkor a teszteléshez mindenképpen használjunk egy tesztadatbázist).

Tipp (vagyis inkább erős javaslat): a tesztadatbázisnak mindig olyat használjunk, amilyen az éles adatbázis is (lehetőleg ugyanazon az adatbáziskezelő szerveren helyezzük el őket), így tényleg eléggé biztosak lehetünk abban, hogy amiket írunk adatmanipuláló és -lekérdező teszteket, azok le fognak futni az adott környezetben.

Én SQLite-ot fogok használni, úgyhogy az újonnan létrejövő .env.testing fájlban a DB_CONNECTION attribútum értékét átírhatjuk sqlite -ra, a többi DB_ kezdetű attribútumot pedig törölhetjük is a fájlból. Az APP_ENV attribútum értékét pedig változtassuk meg testing -re. Mivel ez sqlite-ot fog használni, az azt várja el tőlünk alapértelmezetten, hogy a database mappánkban legyen egy database.sqlite nevű fájlunk, ezért ezt hozzuk is oda létre majd futtassunk egy migrálást, ami ezt az .env.testing fájlt használja: php artisan migrate --env=testing . Ennyi kezdeti beállítás után már foglalkozhatunk a FlightsTest osztályunkkal és hozzuk létre ezt a metódust:

public function test_flight_factory()
{
  $flight = Flight::factory()->create();

  $response = $this->get('/flights/' . $flight->id);

  $response->assertOk();
}

Mit várunk el ettől? Azt szeretnénk, hogy jöjjön létre a Flight factory segítségével egy új repülőjárat. Utána ugye működnie kell annak az útvonalnak, ahol ennek az új repülőjáratnak tudjuk megnézni a részleteit (kvázi a FlightsController show metódusa fut le az adott paraméterrel és visszaadja a felhasználónak a részletező weboldalt). Majd egy 200-as, tehát OK, megfelelő lefutást kell visszaadnia. Ez tehát az elvárás, teszteljük is ezt le. (Most már a saját tesztjeinket építgetjük és futtatjuk a phpunit parancs segítségével, így zavaró is lehet, hogy az ExampleTest osztályok metódusai is mindig lefutnak, amiknek igazából sok jelentőségük már nincsen, úgyhogy akár ezek törölhetők is a projektből, vagy kikommentezhetjük a tesztelő metódusaikat is, és akkor példaként megmaradnak nekünk.)

phpunit tests/Feature/FlightsTest.php

Ez az egy tesztelő metódusunk sikeresen le is futott és létrejött az SQLite-os flights adattáblában az új repülőjárat (ennek lekérdezésére én DB Browser-t használok, de létezik hozzá VSCode-os menedzser is, bármelyiket lehet használni.) Próbáljunk ki még egy dolgot úgy, hogy közben a tesztünket is bővítjük:

use RefreshDatabase;

public function test_flight_factory()
{
  $attribute = ['name' => 'Test Flight'];

  $flight = Flight::factory()->create($attribute);

  $response = $this->get('/flights/' . $flight->id);

  $response->assertOk();

  $response->assertSee('Test Flight');
}

Az osztályon belül használjuk a RefreshDatabase importálását. Ez annyiban lesz a segítségünkre, hogy ha olyan tesztelő metódust használok, amely az adatbázisban hajt végre módosításokat (mint ez az iménti is), akkor a változtatás végrehajtása után törli is a módosításokat, így nem fog megnőni az adatbázisunk mérete és a "szemetelés" sem lesz rá igazából hatással. Az iménti példakód úgy hoz létre repülőjáratot, hogy meghatározom neki a name attribútumot, majd a végén a járat részleteinek lekérésénél ellenőrzöm, hogy ott van-e, látható-e a létrehozáskor megadott név.

A bejegyzéshez tartozó változások Github commit elérése itt található.

A későbbiekben többször elő fog fordulni, hogy előbb létre fogjuk hozni a tesztet, majd utána implementáljuk a megvalósítást. Sőt, egy még látványosabb eszközzel, a Dusk-kal is meg fogunk ismerkedni, amivel még látványosabb lesz a tesztelés, de nem szeretnék még ennyire előre rohanni...