Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Automatizált tesztelés Cucumber framework használatával

Szűrő megjelenítése

Automatizált tesztelés Cucumber framework használatával

Szoftverfejlesztőként mindannyian jól ismerjük a tesztelési piramist, illetve az annak csúnyán elhelyezkedő E2E (end-to-end, azaz végponttól-végpontig végrehajtott) teszteket. Az E2E tesztek a legkomplexebbek, előállításuk a legidőigényesebb és a legtöbb előkészítést is igénylik, hiszen alapvetően ezek a tesztek a rendszer komplex működését (vagy inkább együttműködését) tesztelik, minden komponensének bevonásával, szimulálva az adatbázisával (vagy adatbázisaival) és külső függéseivel való kommunikációt, illetve akár az emberi interakciókat a rendszerhez tartozó frontend alkalmazásokon keresztül. Természetesen továbbra sem egyszerűen arról van szó, hogy kérésekkel bombázzuk az élesben futó alkalmazás stacket, hiszen valószínűleg Kis Pista nem különösebben örülne neki, ha minden egyes nap nyitnánk egy bankszámlát a nevére (hogy csak egy kósza példával éljek), de a tesztkörnyezetünk adatbázisait is könnyedén telepakolhatjuk szeméttel az ismétlődően lefutó tesztekkel. Bár a rendszerünk komponensei fölött jó esetben teljes felügyeletünk van és a tesztek után akár, szintén automatizálva, ki is takaríthatnánk a létrejött adatokat, sok esetben vannak olyan külső függések, melyekre nincs ráhatásunk, a takarítás nem lehetséges a szükséges hozzáférések nélkül, még egy kifejezetten tesztelési célokra fenntartott példány esetében sem. így akármennyire jól is hangzik, hogy az E2E tesztek fogják biztosítani a rendszer teljes integritását, a valóságban ez sokszor néhány, egy UAT (User Acceptance Testing, lényegében az ügyfél bevonásával történő manuális E2E tesztelés a rendszer első élesítésekor, vagy nagyobb új feature-ök bevezetésekor) keretében lefuttatott tesztben kimerül.

Tesztkörnyezet

Ebben természetesen semmi új nincs, a tesztelési piramisnak is vannak olyan változatai, ahol "acceptance testing"-ről vagy "system testing"-ről is szó esik, ezek jobban fókuszált, a rendszerünk azon részére eső E2E-jellegű tesztek, amikre teljes ráhatásunk van. Lehet egy-egy service alkalmazás, frontend alkalmazások, vagy azok kombinációja: a lényeg, hogy a tesztekhez szükségünk lesz egy könnyen elindítható és eldobható, mindentől elszeparált és ismétlődően előkészíthető környezetre, valamint egy tesztfuttatóra, amiben definiálni tudjuk a különböző tesztlépéssorozatokat (vagyis az úgynevezett scenario-kat). Előbbire számtalan kiváló eszköz létezik, minden manapság használt programozási nyelv ökoszisztémájában található rá támogatás, de akár manuálisan is fel lehet őket építeni. Tipikusan egyébként a következő komponensekre lesz szükségünk:

  • A tesztben résztvevő alkalmazások:
    El kell indítanunk egy "steril" környezetben minden olyan alkalmazást, ami részt fog venni a tesztelésben. Amennyiben az alkalmazásokat például Dockerben futtatjuk, nagyon könnyű dolgunk lesz, akár egy Bash script is elég lehet az alkalmazások elindítására, ügyelve persze arra, hogy a függések a megfelelő módon legyenek felkonfigurálva (például az egymással kommunikáló REST servicek címe, adatbázisaik címe, stb.)

  • Eldobható adatbázis(ok):
    Szükségünk lesz olyan adatbázis motoroknak kézzel indított, eldobható példányaira, amikkel az alkalmazások kommunikálnak. Ami még ennél is fontosabb, az adatbázisokat tipikusan nem üres állapottal indítjuk, a részletes teszteléshez általában szükségünk lesz valamilyen alapállapotra, tesztadatokra, melyeken az alkalmazások végre tudják majd hajtani a támogatott műveleteket. Érdemes úgy előkészíteni a tesztadatokat, hogy azok akár a tesztek futtatása közben is eldobhatóak és újrainicializálhatóak legyenek, elkerülve a tesztadatok "szennyezéséből" adódó váratlan mellékhatásokat (röviden: a tesztizoláció fenntartása itt még a unit teszteknél is sokkal fontosabb). Fontos megjegyezni, hogy ha a teszteket már a production bevezetés után írjuk, felmerülhet a gondolat, hogy éles adatokat használjunk a tesztelésre, ettől azonban óva intenék mindenkit: az adatok alapos obfuszkálása nélkül ez önmagában törvénysértő és adatszivárgást okozhat, de egyébként is minimum megkérdőjelezhető gyakorlat.

  • A külső (3rd-party) függések mockolása:
    Amit pedig az ilyen jellegű tesztek esetén sem tudunk elkerülni, az a rendszerünkön teljesen kívül eső komponensek mockolása. Itt azonban nem speciális tesztimplementációkra cseréljük az alkalmazásunk bizonyos komponenseit, hanem tipikusan külső eszközök, mint például a Wiremock segítségével szimuláljuk a külső rendszer viselkedését. Gyakorlatilag a legtöbb esetben ez azt jelenti, hogy készítünk egy olyan requestet, amit a mi rendszerünk generál, ezt eltároljuk a Wiremock-ban és hozzárendelünk egy választ. Így tudjuk biztosítani, hogy egyrészt a rendszerünk minden esetben az elvárt formátumú és tartalmú requesteket generálja, aztán a külső rendszer egy "feltételezett" válaszával megy tovább a feldolgozás. Ez utóbbi természetesen akkor használható igazán, ha korábban legalább néhány releváns választ láttunk és használtunk már az említett külső rendszertől (a legjobb, ha konkrét, tesztkörnyezetben lefuttatott E2E tesztek kérés-válasz példányait használjuk az automatizált tesztekben).

Uborkaszezon

Elindítva mindezeket, és a tesztadatokkal feltöltve az adatbázisokat, már kezdhetjük is a tesztelést, de eddig még nincs több a kezünkben, mint egy átlagos lokális fejlesztői környezet. Most jön a korábban említett tesztfuttató, esetünkben a Cucumber bevezetése. A Cucumber (Uborka) a Gherkin (Ecetes uborka, mégis ki találja ki ezeket a neveket?!) nevű DSL-ből és a mögé írt egyszerű nyelvi feldolgozórendszerből álló tesztfuttató. A különböző unit testing frameworkökhöz hasonlóan képes tesztesetekbe szervezett lépéssorozatok, illetve az azok előkészítésére és lezárására vonatkozó elő- és utófeldolgozó lépések végrehajtására, majd a teszteredmények vizualizálására. A zsenialitását az a megközelítése adja, hogy a Gherkin alapon megírt scenario definíciókat természetes nyelven írhatjuk, melyet akár nem-technikai emberek is könnyen el tudnak olvasni, értelmezni, sőt, akár bővíteni, hiszen pár kötelezően megadandó címkét leszámítva, az egész olyan, mintha tördelt folyószöveg lenne. Az alábbi példa a felhasználói bejelentkezést teszteli a Leaflet stack IDP alkalmazásában:

Feature: Standard user sign-in flow tests

  @PositiveScenario
  Scenario: Successfully signing-in with a registered user

    Given the user identifies with the email address test-user-1@ac-leaflet.local
      And the user uses the password testpw01

     When the user signs in

     Then the application responds with HTTP status FOUND
      And the user is redirected to /

A példán is jól látszanak a Gherkin által előírt feldolgozó utasítások, és a velük tökéletes szinergiában folytatódó lépéssorozat definíciók:

  • A Feature egy tesztkészletet jelöl, csupán a ".feature" fájlon belüli szeparálásra szolgál.
  • A Scenario egy adott lépéssorozatot határoz meg, ez esetben a scenario a felhasználó sikeres bejelentkezése lesz.
  • A Given és az utána következő And címszavak előkészítő lépéseket jelölnek, a BDD (Behaviour-Driven Development) módszertanban meghatározott given-when-then csoportosításra reflektálva. Az itt levő lépések gyakorlatilag a tényleges tesztelt művelet előtt elvégzendő lépéseket foglalják össze, ez esetben a felhasználó beírja az email címét, majd a jelszavát.
  • A When címszó jelzi a tesztelt művelet végrehajtását.
  • A Then (és ugyancsak az utána következő And címszavak) a művelet eredményeképp elvárt állapotváltozásokat ellenőrzik: a böngésző HTTP 302 státuszt kapott a kérésre, és átirányította a felhasználót a gyökérútvonalra, tehát nem történt hiba, a felhasználó immár be van jelentkezve.

Gyakorlatilag ennyi egy scenario, azonban két fontos dolgot meg kell itt még említeni:

  • A Given/When/Then/And címkék jelentésben ugyan különböznek, valójában semmit nem számítanak az olvashatóság javításán túl: így hat természetesen az olvasott scenario, a feldolgozó akár úgy is tudná értelmezni a lépéseket, ha mindenhova csak "And"-et írunk (kérlek, ne tedd).
  • És a rossz hír, hogy emögött nincs semmilyen AI mágia, jelen formájában ha megpróbáljuk lefuttatni a scenariot, egy vaskos hibaüzeneten kívül mást nem fogunk kapni.

Ragasszuk össze a teszteket

A Cucumber az úgynevezett "glue" fájlokat használja arra, hogy a fenti mondatokhoz jelentést rendeljen. Az adott nyelvekhez készült implementációk szintén a glue csomagokat használják például az alkalmazás kontextusának felépítésére, adatbázisok és külső függések elindítására, szóval egy jól felkonfigurált Cucumber glue készlet akár a teljes eldobható tesztkörnyezetünket képes kezelni. A LAGS nevű alkalmazásomat, Java alkalmazás lévén, a JUnit Platform Cucumber integrációjával teszteltem, melynek mindössze ennyi a konfigurációja:

@Suite
@IncludeEngines("cucumber")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "hu.psprog.leaflet.lags.acceptance.config,hu.psprog.leaflet.lags.acceptance.stepdefs")
@ConfigurationParameter(key = FEATURES_PROPERTY_NAME, value = "src/test/resources/features")
@ConfigurationParameter(key = PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true")
public class CucumberAcceptanceTest {
}

A fenti konfiguráció aktiválja a JUnit Platform Cucumber támogatását, beállítja hogy hol találhatóak a glue kódok, illetve a .feature fájlok, amik a fentiekben ismertetett scenariokat fogják tartalmazni. Ahhoz, hogy a JUnit továbbra is el tudja indítani a Spring Boot alkalmazást, szükségünk lesz egy alapkonfigurációra, mely az egyik glue packageben található (hu.psprog.leaflet.lags.acceptance.config), és mindössze ennyit tartalmaz:

@SpringBootTest(
        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
        classes = {UtilityConfiguration.class, LeafletAccessGatewayApplication.class})
@CucumberContextConfiguration
@ActiveProfiles("acceptance")
@Rollback(false)
public class AcceptanceTestConfig {
}

Ez gyakorlatilag egy teljesen átlagos Spring Boot integrációs teszt konfiguráció, melyben az egyetlen szokatlan annotáció az a @CucumberContextConfiguration. Ez jelzi majd a JUnit Platform Cucumber támogatásának, hogy az alkalmazás életciklusát a Cucumber fogja innentől irányítani, tehát a Cucumber ezt a konfigurációs classt is be fogja tölteni mint glue kód, aminek eredményeképp elindul az alkalmazás. A tesztadatbázist ez esetben a resources mappába lerakott data.sql fogja automatikusan inicializálni, ez konkrétan a H2 adatbázisdriver egyik featureje, szóval azzal sem kell vesződnünk (legalábbis ebben az esetben).

A tesztlépések szövegezésének értelmezését a "Step Definition" fájlok adják, melyek természetesen egyszerű Java osztályok. A Cucumber Java alatt kétféle API-t ad, az egyikkel klasszikus Java metódusok definiálásával tudjuk létrehozni a Step Definition-öket, a másikkal lambda kifejezések formájában. A Step Definition osztályok egyszerű POJO-k, a konstruktorukban kell létrehoznunk a definíciókat, például a fenti lépésekhez így néznek ki:

Given("^the user identifies with the email address (.*)$",
        (String email) -> ThreadLocalDataRegistry.put(TestConstants.Attribute.EMAIL, email));

Given("^the user uses the password (.*)$",
        (String password) -> ThreadLocalDataRegistry.put(TestConstants.Attribute.PASSWORD, password));

// ...

When("^the user signs in$",
        () -> ThreadLocalDataRegistry.putResponseEntity(lagsClient.requestLogin()));

// ...

Then("^the application responds with HTTP status ([A-Z_]+)$", (HttpStatus httpStatus) -> {

    ResponseEntity<Void> response = ThreadLocalDataRegistry.getResponseEntity();
    assertThat(response.getStatusCode(), equalTo(httpStatus));
});

Then("^the user is redirected to (/.*)", (String location) -> {

    ResponseEntity<Void> response = ThreadLocalDataRegistry.getResponseEntity();
    String locationHeader = String.valueOf(response.getHeaders().getLocation());
    if (locationHeader.startsWith("http://")) {
        assertThat(locationHeader.endsWith(location), is(true));
    } else {
        assertThat(locationHeader, equalTo(location));
    }
});

Gyakorlatilag a korábban már látott Given/When/Then kulcsszavakat használva (bármelyik matchel egyébként bármelyikre, még az And-ek is, de maradjunk konzisztensek) definiáljuk az illesztendő szöveget mint reguláris kifejezést, megjelölve benne a paramétereket, majd elvégezzük vele a tetszőleges műveleteket. A fenti esetekben például látható, hogy a paramétereket egy thread-local registryben tároljuk, majd azokat felhasználva kérjük a bejelentkezést egy HTTP kliensen keresztül. (Gyors megjegyzés itt: ideális esetben egy headless böngészőben futna a tesztelt oldal és például Selenium driver segítségével "kattintanánk" a bejelentkezésre. Bár a LAGS tesztjei éppen UI-t tesztelnek, pontosan ezt kellene tennem, de mivel anélkül is sikerült megoldanom, nem vesződtem a Selenium bekötésével.) Itt egy pillanatra meg is állnék, nagyon fontos megjegyezni, hogy a Cucumber sajnos nem ad beépített lehetőséget a tesztlépések között létrejövő vagy szállítandó adatok tárolására, erre egy rögtönzött thread-local adattárolót érdemes létrehozni (a thread-local azért fontos, hogy a különböző request threadek között ne keveredjenek adatok). Abban egyébként csak egy ThreadLocal<Map<String, Object>> mező van, a .put(), .get*() metódusok csak ezt írják illetve olvassák.

A minden tesztesetet érintő elő- és utófeldolgozó lépések végrehajtását is Step Definition-ökkel hozzuk létre, például az alábbi módon:

Before(scenario -> log.info("Starting scenario '{}'...", scenario.getName()));
After(scenario -> log.info("Scenario '{}' completed with status {}", scenario.getName(), scenario.getStatus()));
After(ThreadLocalDataRegistry::reset);

Ezzel például minden teszteset előtt látjuk az éppen induló scenario nevét (ez az amit a Scenario kulcsszó után írunk), a végén a scenario eredményét, illetve a tesztizoláció és a tesztesetek közti adatszivárgás megakadályozása céljából kiürítjük a korábban említett thread-local adattárolót.

Gyakorlatilag ezzel készen is vagyunk, már "csak" teszteseteket kell írnunk. Mivel a JUnit Platform integrációjaként fut az egész, Maven és Gradle esetén is a standard build cycle test phase-ében lesznek lefuttatva a tesztek automatikusan, így természetesen számítsunk arra, hogy a build idő jelentősen meg fog nőni. Viszont ezzel már lesz egy automatizált tesztkészletünk, amivel biztosíthatjuk alkalmazásunk stabilitását, annak körülményes és időrabló újra- és újratesztelése nélkül. Az alább mellékelt linkeken további információkat találtok a Cucumber frameworkről, illetve a LAGS nevű alkalmazásomban levő tesztkészletet is megtekinthetitek. A következő cikkemben terv szerint megnézünk egy komplexebb példát TypeScript alatt (sajnos ott némiképp bonyolultabb összerakni mindezt).

A Cucumber Framework weboldala

LAGS autotesztek Cucumberrel

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.