Pár gondolat a CI (Continous Integration) témakörében

Minap a szokásos reggeli beszélgetéseink közben kisebb konfliktus alakult ki a fejlesztők között programozás elméleti kérdések kapcsán… A téma a CI folyamatunk fejlesztése, kiegészítése volt. Rengeteg érv és ellenérv, megállapítás született a beszélgetés alatt, amelyet úgy érzünk jobb megosztani másokkal is, hátha ennek segítségével tudunk jó ötleteket adni más csapatok számára.

Mi is a CI (Continous Integration)?

A CI arra szolgál, hogy a fejlesztés teljes időtartama alatt a fejlesztők által végzett program módosításokat rendszeresen egybeintegráljuk egy fő fejlesztési ágba (pl. master) lehetőleg úgy, hogy az még működjön is.
A (build) folyamat első lépése az ilyen CI eszközök esetében, hogy egy ún. munkaterületre letöltik az adott program verziót. Ezt a folyamatot indíthatja egy webhook, történhet fix előre ütemezett időpontban, de akár az adott forráskód tárolón bekövetkező változás (commit) is. Ezután igazából már mi szabjuk meg, hogy mit tegyen a forráskóddal a build folyamat, mert az, hogy ki tudott checkoutolni egy adott verziót még nem jelent semmit, max. azt, hogy a forráskód kezelő (git/svn) még üzemel. Ezután jöhet az, hogy működésre bírjuk a szoftvert, lehetőleg automatizáltan.

Rendben, de mit értünk az alatt, hogy működik a program, milyen feltételei vannak?

  1. A kód lefordul. Bár az általunk is alkalmazott PHP-ben ilyenről nem beszélhetünk (mivel nincs compile), de pl. Java/C# esetében már ezzel kezdődik minden, ha itt megakad a folyamat (pl. függőségek miatt, fájlok hiányoznak, szintaktikai hiba van), akkor nincs értelme továbbmenni. PHP esetében viszont ezen a szinten legfeljebb a PHP interpreter segítségével szintaktikai ellenőrzést tudunk megkövetelni, szintaktikai hibákra vadászva.
  2. Az ún. Unit teszteken megfelel a program. Az objektumorientált programozásban az objektumok interakcióba lépnek más objektumokkal azok publikus metódusain keresztül. Az alapelv, amelyből kiindulunk az, hogy ha én használok egy másik objektumot, akkor az megfelelően fog működni. Hiszen ha úgy állok hozzá, hogy a többi objektum kiszámíthatatlan működésű, akkor minden osztályom tele lesz olyan kódokkal, ami megpróbálja áthidalni azt, ha a többiek helytelenül végezték a dolgukat… Elvileg az egyes osztályok előre megadott dokumentáció szerinti működést ezekkel az ún. unit tesztekkel tudjuk elérni. Ennek nem feltétlenül van köze a hibákhoz, hiszen azok egy magasabb szinten keletkezhetnek, itt a tesztek azt az interfészt fogják lefedni, amin keresztül a többi objektum interakcióba lép vele. Erre is megvannak a megfelelő, ún. teszt framework-ök, mi PHP esetében pl a PHPUnit-ot használjuk. Ha ezeken a unit teszteken átmegy a módosított program, akkor az aktuális verzióban eszközölt változtatásokkal az egyes objektumok szintjein nem okoztunk nagy gondot (már ha jól vannak azok a tesztek megírva). (Az egységtesztekről, illetve a TDD fejlesztési módszerekről hasznos cikk olvasható itt.)
  3. Az integrációs teszteken átmegy az alkalmazás friss verziója. Az integrációs tesztek már eggyel magasabb szintet képviselnek, mint a unit tesztek. Itt magát az elkészült rendszert fogjuk tesztelni kerek egészként. Erre szoktak önálló tesztkörnyezeteket létesíteni, tesztadatbázissal. Ilyen teszt lehet az, hogy pl. egy webshop esetében, ha betesszük X terméket a kosárba, akkor az valóban megjelenik az aktuális munkamenetben vagy session táblában, ha van hozzá egyedi akciós kuponkódom, akkor az valóban érvényesül az árazás számításánál, stb.
  4. Kézi tesztelés során is megfelelően működik az alkalmazás. Itt a már automatizmusok által teszteken megfelelt, s ezzel kézi tesztelésre alkalmasnak nyilvánított, önálló környezetbe telepített alkalmazás verzióról beszélünk.

Miért kell a CI nekünk?

Vegyük csak sorba egy CI környezet előnyeit:

  • A fenti ismertetett tesztelési fázisok legtöbbjének (tipikusan mindhárom fázisra) elvégzésre számos eszközt kínál.
  • Az egyes forráskód változtatások / ütemezett telepítések során előálló teszteredményekről folyamatos információt tud nyújtani. (Tehát látszik, hogy mely változtatások után melyik teszten hasalt el a program.)
  • A tesztelők számára elkülönített, dedikált tesztkörnyezetbe automatizáltan képes új verziót telepíteni.
  • A tesztelési folyamatok (lásd a fenti 1-2-3 pontokat) minden szakaszáról értesítéseket (pl e-mail) képes szolgáltatni.

Azt hiszem, nem szükséges tovább magyarázni, miért is kell jól működő CI környezet…

Mikor kell a CI?

  • Kis létszámú, vagy egy főből álló fejlesztői csapat (hobby / garázsprojekt): nem kell CI környezet.
    • (Szerk.: És itt rögtön meg is cáfolnám magunkat. Igenis, egy ilyen 1-2 szereplős fejlesztői csapatnál is van értelme legalább az egységteszteknek, de akár az integritási tesztek CI környezetbe szervezésének is. Tipikusan ilyen, dedikált tesztelő nélküli fejlesztéseknél egyszerűen fizikailag képtelenek a kis létszámú fejlesztők minden lehetséges eset tesztelésére, kimutatható módon lassítja őket az állandó manuális tesztelés igénye. (Vagy egyáltalán nem foglalkoznak a teszteléssel, és a végfelhasználói tesztre bízzák magukat, de az nemcsak arcvesztéssel jár, de rendkívül lassú visszacsatolást jelent, ami növeli a projekt időtartamát, következésképpen a költségét is. Remek példa erre az egyik múltkori alkalmazásunk: egyszerű CRUD folyamatok kapcsán több, mint 40 teljesen hasonló felépítésű, funkcionalitású képernyőt kellett készítenünk, amit természetesen közös, általános feladatkezelő osztályokkal próbáltunk meg lefedni, azonban már a fejlesztés legelején Selenium alapú PHPUnit teszteket írtunk pl. a lapozást kezelő Pagination osztály működésére: A Selenium-ban készült egy teszt forgatókönyv, amely egy tetszőleges adatlistázó képernyőn megadott szűrőfeltételeknek megfelelően kilistázta az adatokat, miközben a háttérben az adatok forrását kezelő osztály segítségével folyamatosan szúrt be, inaktivált, törölt, módosított elemeket. A Selenium tesztben ilyenkor például azt írtuk le, hogy ilyen esetben a Pagination osztály egy metódusának milyen visszaadott kilistázható elemszámot, és maximális oldalszámot kell szolgáltatnia és az adott képernyőn lévő lapozó elem összes/hátralévő/aktuális oldalszámának kijelzése hogy kell, hogy kövesse ezeket a változásokat. A valódi képernyők fejlesztése során ezt az előre megírt forgatókönyvet kellett „csak” felhasználnunk annak ellenőrzésére, hogy mind a több, mint 40 képernyőn megfelelően működik a lapozás kezelése. Ha nem alkalmaztunk volna egységteszteket és automatizált felhasználói felület teszteket, úgy ezt minden új képernyő fejlesztése során egyesével kézzel kellet volna ellenőriznünk, ami ténylegesen kimutatható költségráfordítással járt volna.)
  • Nincs dedikált tesztelő szerepkör, csak a fejlesztők tesztelnek: nem kell CI környezet.
  • Stabil üzleti logika, a program specifikációja garantáltan nem fog változni: nem kell CI környezet.
  • Garantáltan soha nem kell később elővenni a kódot, átadás után eldobható az egész: nem kell CI környezet.

Minden más esetben azonban nem árt. 🙂 Egy jól összerakott CI környezet nemcsak akkor hoz eredményt, ha kellően sokan dolgoznak egyszerre a projekten, illetve elkülönült szerepkörök (fejlesztő/tesztelő) vannak. Bár elsősorban sokáig életben tartandó, sok mindennel kommunikáló nagyobb üzleti funkciót lefedő alkalmazásoknál van igazán értelme, de jól megtanulni kis projekteken lehet, ahol kisebb a komplexitás és esetleg a projekt határidői is lehetővé teszik azt a plusz erőforrás igényt, amelyet ennek a kezelése jelent.

Mit teszünk a CI folyamatunk bővítése érdekében?

Először is elhatároztuk, hogy a fejlesztési folyamatba beépítünk egy CI eszközt, konkrétan a Jenkinst: Segítségével fogjuk össze a tesztelési fázisokat és automatizáljuk az egyes feladatokat a lehető legnagyobb mértékben. A Jenkins minden egyes build folyamat során a következőket fogja elvégezni:

  • Ellenőrzi, hogy minden PHP állomány szintaktikailag helyes-e.
  • A PHPMessDetector plugin segítségével potenciális hibákat keres a forráskódban.
  • A PHPCheckStyle plugin segítségével ellenőrzi a kódolási konvenciókat.
  • A PHPCopyPasteDetector plugin segítségével ellenőrzi, nem maradt-e olyan programrész a forráskódban, amely felesleges másolása/ismétlése más már meglévő részeknek.
  • A PHPUnit plugin segítségével a már elkészített egységteszteket lefuttatja.
  • A Selenium plugin segítségével elvégzi a felhasználói felületi teszteket.
  • Végezetül a folyamat végén egy tájékoztató e-mailt küld sikertelenség esetén benne a részletes információkkal.

Másodszor pedig elhatároztuk, hogy a meglévő Codeigniter-re épülő egyedi saját keretrendszerünk eddig is rendelkezésre álló egységtesztjeit folyamatosan bővítjük. A Selenium plugin segítségével a meglévő PHPUnit egységtesztek mellé felhasználói felületi teszteket is készítünk minden olyan esetben amikor erre szükség van.

Összegzés

A fent vázolt megoldás csak egy lehetséges megoldás  a PHP / Continous Integration témakörben. Naponta változnak a rendelkezésre álló eszközök lehetőségei és tudásai. Célunk a változtatásokkal a fejlesztett alkalmazások minél szélesebb körű teszt lefedettséggel történő ellátása a folyamatos minőségbiztosítás érdekében. Bízunk benne, hogy a folyamatosan bevezetésre kerülő új módszerek segítségével még jobban ki tudjuk szolgálni ügyfeleink elvárásait még gyorsabb fejlesztési ciklusidő alkalmazásával.

Ha tudsz, vagy gyakorlatod van más CI környezet alkalmazásáról PHP környezetben keress bennünket, szívesen vesszük ha megosztod velünk tapasztalataid, akár a Facebook oldalunkon, akár közvetlenül is.