• email
  • facebook
  • linkedin
  • google+
  • pinteres

GUI tesztelés Abbottal DSL-ek segítségével

1.   Bevezetés

Az alábbi leírással két dolgot szeretnék bemutatni: egyrészt, hogy hogyan lehet Abbottal GUI alkalmazásokat tesztelni, másrészt, hogy mely esetben lehet Java projektekben leghatékonyabban DSL implementálásával megoldani egy problémát. Amit a részletes megvalósításról írok, az közvetlenül az Abbotra vonatkozik, de a design alapelveket, a DSL-ek absztrahálását eszközöktől függetlenül minden UI tesztelés során ugyanúgy alkalmazhatjuk.

Aki nem igazán tudja mi az a DSL, illetve, hogy mit is akarok én ezzel, annak ajánlom olvassa el ennek a leírásnak a tulajdonképpeni bevezetőjét, amely a domain specifikus nyelvekkel foglalkozik.

Ami a GUI tesztelés oldalát érinti: komplex alkalmazások esetében hiába írunk unit teszteket, az alkalmazás működésének szintjéről is mindenképpen tesztelnünk kell az kódot (lehetőleg automatikusan és rendszeresen), mind az alkalmazás logikája mind pedig a felület-logika konzisztenciájának ellenőrzése céljából. Igaz ez webes és standalone alkalmazások esetében is. Swinges (és SWT-s) felületek tesztelésére szolgál az Abbot nevű eszköz, melynek egy lehetséges használati módját mutatom be a következőkben.

Az Abbot library egy önálló kis alkalmazással, a Costelloval együtt érkezik, amely egy teszt rögzítő program. A segítségével készülő programunkat nyomogatva készíthetünk felvételeket, amelyeket teszt szkriptekként elmentve futtathatunk. Bár látszólag jópofa kis eszköz a Costello, hosszú távon és nagyobb mennyiségben nem igazán alkalmas GUI tesztjeink elkészítésére, több okból sem:

  1. A tesztek nagyon nehezen tarthatók karban. A programunkon végzett kisebb-nagyobb változtatások után nagyon nem hatékony a kész szkriptek módosítása, vagy új felvételek készítése.
  2. Alkalmazásunk nagy valószínűséggel végez költséges műveleteket (lokális vagy távoli erőforrások kezelése: szerverhívások, fájlfeldolgozás stb.), amit, ha jól csináljuk, akkor természetesen külön háttér szálban (pl. SwingWorkerrel) futtatunk. Ezen műveleteknek az időtartama nem tudható előre, így a visszatérésükre való várás konfigurálása egy felvevő eszközzel nagyon nehézkes.
  3. A felület működésének valószínűleg lesznek kiemelt részei, amelyeket többször, alaposan is tesztelünk, míg az adott teszt szempontjából kevésbé releváns szakaszokra egységes megoldást szeretnénk. Azaz: sok tesztünknek lesz közös része, akár nagymértékben is. Ezek karbantartása szkriptek szintjén kész lehetetlenség.

Világos, hogy a Costello fő vonzerejét az adja, hogy nem fejlesztők, sőt, esetleg a megrendelő is készíthet teszteket. Ez sok esetben lehet nagyon hasznos, de valószínűleg minél izgalmasabb a projektünk, annál kevésbé működik kizárólagosan. Lássuk most inkább hogyan használható Abbot Costello nélkül a legkényelmesebben, azaz milyen Abbot egyedül.

2.   Demó

Abbot demo

Az Abbothoz is készítettem kis demó alkalmazást, amely letölthető az SVN-ből. A demó futtatásához az AbbotDemotTest.java fájlt kell futtatni JUnit tesztként, vagy az ant AbbotDemotTest parancsot kiadni. Mindenkinek ajánlom, hogy mielőtt folytatná a leírást futtassa a demót, és nézze meg, mit is tud az Abbot, látványos!

3.   A felület és a teszt

A tesztelendő felület nagyon egyszerű: a kis javaee demó alkalmazásra tettem egy fület, Abbot néven, amelyen két gomb található, Form 1 és Form 2 felirattal, amelyek mindegyike egy ugyanolyan formot nyit meg. A formok leokézása után a program egy szerverhívást imitál, amely hívás véletlenszerűen 1-3 másodpercig tarthat. A szerverhívás futása alatt a gombokat letiltjuk, és az alattuk található JProgressBaron jelezzük a futását. Tesztelés szempontjából az jelent majd megoldandó problémát, hogy meg kell várnunk a szerverhívás visszatérését, hogy a gombot be tudjuk nyomni.

A szerverhívást természetesen külön szálon, a 6-os Javaban már alapértelmezetten megtalálható SwingWorker segítségével futtatjuk:

void imitateServerCall() {
    form1Button.setEnabled(false);
    form2Button.setEnabled(false);
    progressBar.setIndeterminate(true);
    new SwingWorker<Void, Void>() {

        @Override
        protected Void doInBackground() throws Exception {
            int delay = (random.nextInt(3)+1)*1000; //millisecs
            Thread.sleep(delay);
            return null;
        }

        @Override
        protected void done() {
            progressBar.setIndeterminate(false);
            form1Button.setEnabled(true);
            form2Button.setEnabled(true);
        }

    }.execute();

}

A teszt lépései:

  • az alkalmazás elindítása
  • a megfelelő fül kiválasztása
  • az első form ablak megnyitása, kitöltése és bezárása, a szerverhívás elindulásának megvárása
  • a szerver hívás kivárása
  • a második form ablak megnyitása, kitöltése és bezárása, a szerverhívás elindulásának megvárása
  • a szerver hívás kivárása

4.   A megvalósítás

Az alkalmazás nagyon egyszerű és a teszt is könnyen megírható különösebb design nélkül. Azonban már a feladat tisztázása után is látszik, lesznek közös lépések, amelyeket nyilván szeretnénk kiemelni.

Az Abbot működését tekintve alap szintű műveletekre épít. A Swing komponens hierarchiát tudja bejárni, ott komponenseket képes megtalálni, és azokon műveleteket tud elvégezni a komponens típusa szerint megfelelő helper osztály segítségével. Ha tehát van egy JTextFieldünk, amelybe írni szeretnénk, akkor azt meg kell találnunk. Erre a legkényelmesebb mód a minden java.awt.Component leszármazottban megtalálható String name property beállítása a kódban, amely alapján könnyen azonosíthatjuk majd a komponenst a fában. Az erre építő beírós logikát ki is emelhetjük egy általánosabb szintre:

protected static void enterTextInField(Container container,
        String fieldName, String text) throws Exception {
    Component field = finder.find(container, new NameMatcher(fieldName));
    textComponentTester.actionEnterText(field, text);
}

Fenti metódus okossága még, hogy nem a teljes fában fog keresni, hanem egy alsóbb elemtől lefelé. Ez azért hasznos, mert mivel mindkét ablakon lévő form azonos, elemeinek is ugyanaz a name értéke, ám a teszt során valahogy mégis az aktuálisan láthatóra kell referenciát szereznünk. A legegyszerűbb megoldás, ha mindig csak az éppen megnyitott ablakon keresünk.

Az idézett metódus tulajdonképpen egy példa, amely azt mutatja, bizonyos alap műveleteket érdemes kiemelnünk. Számos ehhez hasonló esetet találhatunk, például: pop-up ablak bezárása, checkboxra kattintás, rádiógombra kattintás, combobox elemének kiválasztása érték alapján, combobox elemének kiválasztása index alapján stb. Sok tucatnyi példát lehetne mondani, de ennyi is elég, hogy lássuk: nem csinálunk mást az említettek implementálásával, mint egy absztrakciós réteget definiálunk, amelyen keresztül a felületet manipulálni tudjuk. Az ezen absztarkciós réteg szintjén definiált metódusain pedig tulajdonképpen egy belső DSL-t, azaz domain specifikus nyelvet adnak, amely nyelv tartománya (domainje) az alkalmazásunk felületének kezelése a komponensek szintjén. Az demó alkalmazásban ezeket a metódusokat tettem a UILevelDsl osztályba, ezzel is jelezve, hogy a UI manipulásának szintjén járunk még.

Miután elkészült a komponensek kezelésére szolgáló nyelvünk, átléphetünk az üzleti logika szintjére. Itt a feladatok szintén nagyobb csoportokba oszthatók. Már nem csakarról van szó, hogy egy-egy mezőt ki szeretnénk tölteni, hanem pl. cím adatokat szeretnénk megadni, a megrendelés kosarat össze akarjuk állítani stb. Ezen lépések mindegyike több műveletből áll a UILevelDsl szempontjából, mégis kisebbek a teljes alkalmazás használati workflowjának szempontjából. Ebben a kényelmetlenül tág rétegben ismét akkor járunk a legjobban, ha újabb absztrakciós réteget, és az annak tartományára érvényes DSL-t definiálunk. Példámban ez a BusinessLevelDsl osztály tartalma. Ennek metódusai (szavai) az üzleti logika szempontjából értelmesek, az implementációjuk pedig a UI szintű DSL hívásait tartalmazza:

public static void fillForm1() throws Exception {
    Dialog dialog = openDialogByButton("AbbotDemo.form1Button", "AbbotDemoDialog");
    enterTextInField(dialog, "AbbotDemoDialog.egyTextField", "Első szöveg");
    seletInCombobox(dialog, "AbbotDemoDialog.kettoComboBox", 3);
    clickCheckBox(dialog, "AbbotDemoDialog.haromCheckBox");
    clickButtonByName(dialog, "AbbotDemoDialog.negyRadioButton");
    closeDialogByButtonLabel(dialog, "OK");
}

Az idézett metódus az első form ablak kitöltését végzi. Egy tényleges alkalmazás esetében inkább ilyen utasításokkal találkozhatunk a nyelvben, mint createOrder(), createOrder(partner, basket), createPartnerWithDefaultAddress(), createPartner(address), payByCard, payWithCash stb. Az üzleti logika absztrakciós szintjén definiált nyelv segítségével pedig már könnyen készíthetünk külön teszteket, amelyek számos közös résszel rendelkeznek, például csinálhatunk több megrendelést, ugyanannak az ügyfélnek különböző kosárral, vagy mondjuk fizethetünk egyik esetben készpénzzel, a másikban kártyával.

Az absztrakciós rétegek bevezetésének leginkább csak a józan ész szab határt. Összetett, sokablakos alkalmazások esetében érdemes lehet például külön osztályokba tenni az egyes ablakok manipulálására vonatkozó felületi logikát, amely a UI szintű DSL-t használja, és az üzleti logika DSL-ének adja az alapját. Ezzel egy következő szintet vezettünk be a példánkban leírt két szint közé.

5.   Pár jótanács

Pár dolog még az Abbottal kapcsolatban:

  • A szerver hívások kivárására a legegyszerűbbként kínálkozó megoldást, a progressbar figyelését választottam. Amikor az állapotot vált, tudhatjuk, hogy a szerverhívás elindult, vagy visszatért. Elképzelhető, hogy ez nem mindig megfelelő, ilyenkor érdemes lehet az Abbot számára hozzáférhetővé tenni egy alkalmazás szintű változót (azokat tároló osztályt), amelynek segítségével az állapotok változásait követheti.
  • Ez a kivezetés azért is hasznos lehet, mert (amit itt a demóban nem tettem meg) a tesztjeink így hozzáférhetnek a felületen létrehozott objektumokhoz (pl. kosár, megrendelés, cím vagy partner), és azokon további ellenőrzéseket végezhetnek.
  • Ne legyünk restek saját Matcher és Condition implementációkat írni (a UILevelDsl-ben található ezekre is pár példa). Segítségükkel a legapróbb elemek szintjéig követhetjük és irányíthatjuk a teljes felület viselkedését.
  • Ne feledjük, hogy egy komponens akkor is könnyen lehet, hogy a UI hierarchiában van, ha éppen nem látszik, így előfordulhat, hogy azonos névvel egy másik példányt talál meg az Abbot, és azt próbálja módosítani, nem pedig azt, amelyikre mi számítunk.