2011. október 17., hétfő

Page Object minta alkalmazása Seleniummal

A múltkori elméleti bevezető után jöjjön egy kis gyakorlat a Page Object tervezési minta Seleniummal való használatához.

Nézzünk is meg egyből egy rövid kódrészletet, amit mindenféle módosítás nélkül a Selenium IDE-ből exportáltam ki! Látható hogy a Selenium parancsok és az ellenőrzési feltételek nem válnak el egymástól, továbbá a várakozási feltétel implementációja sem valami jól olvasható, ami pedig kifejezetten zavaró ha a teszteseteink ezzel vannak tele.
selenium.open("/portal/ep/home.do");
selenium.type("password", password);
selenium.type("username", userName);
selenium.click("css=input[type=image]");
for (int second = 0;; second++) {
 if (second >= 60)
  fail("timeout");
 try {
  if ("szöveg1"
    .equals(selenium.getText("css=#sfeBranchDipatch > div")))
   break;
 } catch (Exception e) {
 }
 Thread.sleep(1000);
}
assertEquals(selenium.getText("css=strong"), "Szia " + userName);

Miután a fenti kódot átírtam a Page Object minta alkalmazásával, egy jól olvasható tesztesetet kaptam. Azt hiszem a két kódrészletet összehasonlítva az eredmény magáért beszél. A továbbiakban lássuk mi is bújik meg a Seleniumos Page Objectek mögött!
Home home = new Home(selenium);
InfoPortal infoPortal = home.loginUser(userName, password);
assertEquals(infoPortal.getHeaderMsg(), "Szia " + userName);
AppPage appPage = infoPortal.getMenu().goToAppPage(selenium);
Először is érdemes minden Page Object-et egy Page osztályból származtatni, ahova az oldalakkal kapcsolatos közös részeket emeljük ki. A valódi webes oldalakhoz hasonlóan ahol is minden oldalon megjelenik egy menüsor a Page osztályból is elérhető egy Menu objektum, amely a navigációhoz szükséges menüelemeket tartalmazza. Ezenkívül itt kap helyet egy Selenium objektum is, melyet a Page Object-ek konstruktoraiban adunk majd mindig át.
public class Page {
 protected Selenium selenium;
 private Menu menu;

 public Page(Selenium selenium) {
  this.selenium = selenium;
  menu = Menu.getInstance();
 }

 public Menu getMenu() {
  return menu;
 }

 public void setMenu(Menu menu) {
  this.menu = menu;
 }
}
A Menu osztály a Singleton tervezési mintára épül, mivel a tesztesetek egészére nézve globálisan csak egy példányra lesz belőle szükségünk! 
public class Menu {
 private static Menu menu = null;

 private Menu() {
 }

 public static Menu getInstance() {
  if (menu == null) {
   menu = new Menu();
  }
  return menu;
 }

 public AppPage goToAppPage(Selenium selenium) {
  selenium.click("css=#mainTab3 > span");
  return new AppPage(selenium);
 }
}
Az adatforrásból kinyert mezők paraméterátadás útján jutnak el a Page Object-ekben található akció metódusokhoz, mint amilyen az Home loginUser() metódusa. Fontos megkötés, hogy az akciók visszatérési értéke mindig a következő oldalt jelentő page osztály legyen, ami jelen esetben az InfoPortal.
public class Home extends Page {
 public Home(Selenium selenium) {
  super(selenium);
 }

 public InfoPortal loginUser(String userName, String password)
   throws Exception {
  selenium.open("/portal/ep/home.do");
  selenium.type("password", password);
  selenium.type("username", userName);
  selenium.click("css=input[type=image]");
  waitForElementPresent("css=#sfeBranchDipatch > div",
    "Az kívánt oldal ….");
  return new InfoPortal(selenium);
 }
}
A Page Object minta egyik alapvető koncepciója, hogy a Page osztályok ne tartalmazzanak ellenőrzési feltételeket, azaz asserteket. Ez alól kivételt jelentenek az olyan jellegű megkötések, melyeknek az oldalra lépéskor mindig teljesülnie kell. Ilyen ellenőrzési feltétel található az InfoPortal konstruktorában, amely feltételezi hogy a kijelentkezés link mindig elérhető az adott oldalon.
public class InfoPortal extends Page {
 public InfoPortal(Selenium selenium) {
  super(selenium);
  assertEquals(
  selenium.getText("//table[@id='countDownTable']/tbody/tr/td[2]/a"),
  "Kijelentkezés");
 }

 public String getHeaderMsg() {
  return selenium.getText("css=strong");
 }
}
Ahogy korábban említettem, a waitFor kezdetű parancsokhoz a Selenium IDE nem generál valami szép kódot, így a kód olvashatósága miatt és a duplikációk megszüntetése érdekében ezeket érdemes kiemelni egy külön segéd osztályba vagy akár a közös Page osztályba.

Röviden ennyi a Page Object minta implementálásáról, a következő bejegyzésem pedig már a zárócikke lesz a Selenium-os sorozatomnak. Elöljáróban annyit, hogy a Selenium-os adatforrás kialakításról lesz szó, azonban egy olyan egyedi megoldást fogok bemutatni, amivel máshol még biztos nem találkoztál!

2011. október 2., vasárnap

Karbantartható Selenium tesztek készítése

A Selenium IDE segítségével kiexportálható teszteset tipikusan struktúra nélküli "spagetti kód", melyet nehéz olvasni és bővíteni, továbbá az oldalon végrehajtott módosítások is könnyen elronthatják az összes eddigi tesztjeinket a megváltozott lokátorok miatt.

Tegyük fel, hogy készítettünk 100 Selenium tesztesetet melyek mindegyike egy bejelentkező oldalról indul ki. Ha a bejelentkező oldalon, a felhasználónév megadására szolgáló input mező fölé bekerülne egy új input mező és az XPath position alapú lokátor stratégiát használtuk, akkor a //div/input[0] helyett //div/input[1] –re változik a felhasználó név mezőhöz tartozó relatív XPath kifejezés. Mivel mind a 100 teszteset erről az oldalról indul ki, ezért az összes tesztünket módosítani kellene, ami elég időigényes feladat.

Az előbb vázolt problémából kiindulva, az első gondolat az Extract Method minta alkalmazása lenne, miszerint a tesztosztályon belüli ismétlődő kódrészeket kiszervezhetnénk külön private metódusokba. Ezzel viszont az lenne a gond, hogy a kód duplikációt csak az osztályon belül kezelnénk le, azaz a tesztosztályok közötti kódismétlődés ettől még nem szűnne meg. A megoldás a Page Object minta alkalmazása, amellyel az oldalakhoz tartozó kódrészek, a nekik megfelelő un. page osztályokba kerülnek kiszervezésre.

Ha megvizsgálunk néhány Selenium IDE által rögzített tesztesetet, megállapítható hogy akciók hatására oldalakra navigálunk és az oldalakon ellenőrző feltételeket értékelünk ki. Az alábbi ábrán az akciókat a piros dobozok, míg az oldalakat a kék karikák szemléltetik.


A Page Object minta szerint, az egyes oldalakhoz tartozó műveleteket külön Page osztályokba kell kiszervezni (pl.: bejelentkeztetés -> LoginPage), az ellenőrző feltételek és az oldalak közti navigációs kódok pedig a tesztmetódusokban kapnak helyet, mivel így biztosítható a Page Objectek későbbi újrahasználhatósága. Az alábbi ábrán látható szekvencia diagram jól szemlélteti a Page Object minta Selenium környezetben való használatát.


A fenti ábra alapján, a tesztmetódus meghívja a LoginPage "Page Object" bejelentkeztető metódusát átadva a felhasználó nevet és a jelszót, majd a beléptetés után a következő oldalt jelentő HomePage "Page Object" példánnyal tér vissza, melyen a tesztmetódus meghívja annak openTab() metódusát. Bár az ábrán nincs jelölve, a bejelentkezés után visszakapott HomePage objektumon a tesztmetódus ellenőrzési feltételeket végezhet a kezdőlapon megjelenő felhasználó név helyességére vagy a felhasználó által látható menüelemekre vonatkozólag.

Visszatérve a fejezet elején említett problémára, ha a Page Object tervezési mintát alkalmazzuk, akkor a //div/input[0]-ről, //div/input[1]-re történő megváltozás esetén, nem kell mind a 100 tesztesetünket módosítani, csupán a LoginPage "Page Object" bejelentkeztető metódusánál kell átírni a felhasználónév input mező kikeresésére szolgáló lokátort.

Összefoglalás a Page Object minta használatáról
  • Oldalanként vagy oldalon belüli komponensekre tovább bontva hozzunk létre a Page Object-eket, melyek az akciókat tartalmazzák.
  • A Page Object akció metódusainak visszatérési értéke mindig ahhoz az oldalhoz tartozó Page Object legyen, ahová a navigáció irányítja majd a felhasználót.
  • Mindig a tesztesetek tartalmazzák a visszakapott Page Objectre – vagyis az oldalra - vonatkozó ellenőrzési feltételeket.
  • A Page Object-ek ne tartalmazzanak ellenőrzési feltételeket, kivéve olyan jellegű assertek-et, melyek az egész oldalra vonatkoznak, és mindig teljesülniük kell. (pl.: az adott oldalon megjelenő böngésző címe, aktív menü ellenőrzése az oldalra lépésekor).
  • A tesztesetek ne tartalmazzanak Selenium specifikus kódot, az assertek végrehajtásához szükséges lokátor kódot illesszük be a megfelelő Page Object-be, getter metódusként.
  • Kezeljük kiemelt fontossággal a Page Object-ek névadását, hogy megkönnyítsük azok beazonosítását!

A Page Object minta előnyei és hátrányai
  • Duplikációk kiküszöbölése azáltal, hogy a módosítások esetén csak a Page Objectekhez kell hozzányúlni a teszt metódusokhoz nem.
  • A tesztesetekben nem jelennek meg Selenium specifikus kódrészletek, így a mögöttes Selenium API bármikor lecserélhető. Ez a későbbiekben megkönnyíti majd a Selenium 2 API használatára történő átállást.
  • Újrafelhasználhatóság biztosítása azáltal, hogy a tesztkód és az oldalhoz tartozó kód elválik.
  • Kellő számú Page Object létrehozása után, automatikusan kialakul egy saját API, amellyel azonnal létrehozhatunk a felületi teszteket a Selenium IDE használata nélkül is!
  • A minta hátránya, hogy a fejlesztőtől többletmunkát igényel a Page Object struktúra kialakítása, ami persze a későbbiekben bőven megtérül.

A következő bejegyzésemben a Page Object minta Selenium környezetben való alkalmazását fogom bemutatni egy konkrét példán keresztül.