2011. november 19., szombat

Lejárt JRebel licence ingyenes megújítása

Nemrég írtam egy ismertetőt a JRebel-ről, de akkor nem említettem hogy sajnos csak 30 napig használható ingyenesen. Ha ezután is használni szeretnénk, frissíteni kellene a licence-t, ami bizony komoly költséget is jelenthet. Szerencsére erre is van megoldás - méghozzá kettő is -, így most megosztom ezeket a blogom olvasóival!

Az első lehetőség, hogy kérünk a JRebel-től egy új licence-t. Persze nem a meglévő meghosszabbítását, hanem egy újat! :) A JRebel legelső telepítését normál módon végezzük el a manual alapján, amihez használhatjuk a jar installert vagy az Eclipse plugin-on keresztüli telepítést is. Miután lejárt a licenszünk, látogassunk el a http://www.zeroturnaround.com/jrebel/eclipse-eval/ oldalra, ahonnan igényelhetünk egy újat. Itt csupán annyi a megkötés, hogy egy e-mail címet csak egyszer regisztrálhatunk be az új license kéréséhez. Használjuk a YOPMail-t. Ezután email-ben csatolva megkapjuk a jrebel.licence fájlt amit másoljuk be a JRebel telepítési könyvtár alá, ami nálam pl. a /usr/local/ZeroTurnaround/JRebel könyvtár. Ezzel a kis trükkel újabb 30 napig használhatjuk a JRebel-t! Szuper! :)

A második lehetőség pedig a JRebel Social használata, amit bárki ingyenesen alkalmazhat nem kereskedelmi jellegű alkalmazások fejlesztéséhez. Örülök, hogy a JRebel Team meglépte ezt a lépést a Java közösség érdekében!


2011. november 1., kedd

Adatforrás kialakítása Selenium tesztekhez

A mostani cikkemben a FitNesse és a Selenium keretrendszerek egy olyan előnyös ötvözéséről fogok blogolni, amit jómagam találtam ki a Selenium-os tesztelés megkönnyítésére!

Gyakori igény, hogy szükségünk lenne a Selenium tesztjeink eltérő adatokkal való futtatására. Az adatok tárolására elsőre jó megoldásnak tűnhet egy egyszerű csv, properties vagy excel táblázat használata, azonban több száz teszt esetén ezek nem lesznek karbantarthatóak (gondoljunk csak bele, hogy milyen nagy állományok jönnének létre), főleg amikor eltérő környezetekhez eltérő adatokat kellene használnunk. Egy javítási mód lenne ha nem egy nagy excel táblázatot kezelünk, hanem környezetenként (esetleg tesztenként) létrehoznánk egyet-egyet, azonban egy idő után ezzel is karbantartási problémák merülnének fel, mivel a sok állomány manuális kezelése elég nehézkes!

A felvázolt problémára szerintem egy webes oldalstruktúrát használó, HTML táblázat alapú adatforrás jelenthetné a megoldást. Mivel kifejezetten erre a célra nem készült még keretrendszer, úgy gondoltam, - az eredeti felhasználási területétől egy kicsit elrugaszkodva - hogy bevetem a FitNesse-t!

Excel alapú Selenium adatforrás

Bár nagyszámú teszteset esetén az excel alapú adatforrás véleményem szerint nem a legmegfelelőbb választás, bizonyos esetekben megfelelő alternatíva lehet az egyszerű és gyors kezelhetősége miatt. Az excel alapú adatforrás megvalósításának részleteiről itt olvashatsz bővebben.

FitNesse alapú Selenium adatforrás

A Selenium és a FitNesse integrációjával nemcsak az adatforrás problémakörre kapunk megoldást, hanem egy-két nagyon hasznos feature-re is szert teszünk:

  • Tesztjeinket a FitNesse webes felületéről is elindíthatjuk és megtekinthetjük lefutási eredményeket.
  • A FitNesse wiki oldalak verziózásából adódóan, visszaállhatunk egy korábbi adatforrás verzióra.
  • A tesztek definiálása és az adatok megadása mellett, akár szöveges megjegyzéseket is hozzáadhatunk az adatforrás oldalaihoz.
  • Visszakövethetők azok a felhasználók, akik az adatforrás oldalain módosításokat végeztek.
  • Az oldalak szerep alapú korlátozásának lehetősége az olvasás, írás és teszt futtatás tekintetében.

A FitNesse alapú adatforrás kialakításának további előnye, hogy az eltérő környezetekben futó tesztek (pl.: Teszt, Fejlesztői, Integrációs) nemcsak eltérő adatforrásokat, hanem egyedi paramétereket is használhatnak melyeket a hierarchia tetején is definiálhatunk az öröklődésük miatt.


A fenti ábra a FitNesse segítségével definiált, Selenium tesztekhez kapcsolódó adatforrások elhelyezkedését mutatja. A RootSuite tartalmazza a környezetek szerinti suite-okat (TestSuite, DevSuite, IntSuite), melyek a tesztesetek adatforrásait tartalmazzák. A kialakított adatforrás felépítés következtében, a tesztek a gyökér suite-nál egyszerre, vagy környezetenként külön-külön is indíthatók. Érdemes a RootSuite-nál meghatározni a Fixture classpath-t és a használni kívánt böngésző típust (driver), a környezeteknek megfelelő suite-oknál pedig a baseUrl-t, mivel a szülő csomópontoknál megadott paramétereket tovább öröklődnek a gyerekek felé.

Az adatforrások létrehozása megegyezik a FitNesse teszt táblázatok felvételével, a különbség annyi hogy a tesztmetódusok visszatérési értékét nem ellenőrizzük, így azok egységesen true-val térnek vissza. Természetesen a sikertelenül lefuttatott tesztesetek ilyenkor is kijelzésre kerülnek. 


A környezetenként eltérő ${baseUrl} és a böngésző meghajtóhoz tartozó ${driver} változóként kerül definiálásra, mivel az értékek a hierarchiában feljebb lévő suite-nál kerültek definiálásra.


Az elkészített wiki oldalhoz tartozó adatokat a LoginTest, testLoginFunction metódusa használja fel. A tesztosztály a TestBase ősosztályból származik, ahol inicializálásra kerül a baseUrl és a böngésző típus. A TestBase egyben egy ColumnFixture is, amit a RootSuite-nál definiált classpath bejegyzés köt össze a wiki oldallal.
public class TestBase extends ColumnFixture {

 protected Selenium selenium;
 protected WebDriver driver;

 protected void beforeTestCase(String[] fixtureArgs) {
        String baseUrl=fixtureArgs[0];
        String driverName=fixtureArgs[1];
        
        if(driverName.toLowerCase().equals("firefox")){
            driver = new FirefoxDriver();
        }
        else if(driverName.toLowerCase().equals("chrome")){
            driver = new ChromeDriver();
        } 
        //...

        selenium = new WebDriverBackedSelenium(driver, baseUrl);
    }

 protected void afterTestCase() {
  driver.close();
 }
}
Az így kialakított felépítés nem egy hagyományos JUnit teszteset, így a beforeTestCase() ill. afterTestCase() metódusokat a tesztmetódusban kell meghívni. Az afterTestCase() metódus a finally blokkban kapott helyet, így annak lefutása mindig garantált, azaz a Selenium által megnyitott böngésző egy esetleges kivétel esetén is bezárásra kerül. A teszt elindításával a LoginTest publikus mezői megkapják - a táblázat minden sorára lefutva - a wiki táblázat megfelelő mezőinek értékét, megoldva a külső adatforrás értékeinek a felhasználását.
public class LoginTest extends TestBase {

 public String userName;
 public String password;

 public boolean testLoginFunction() throws Exception {
  beforeTestCase(getArgs());
  try {
   selenium.open("/portal/ep/home.do");
   //További selenium parancsok vagy PageObject-ek használata
  } finally {
   afterTestCase();
  }
  return true;
 }
} 
Ezzel egyelőre véget is ért a Selenium-os cikksorozatom. Ha hasznos infókat tartalmazott a számodra, esetleg további kérdéseid lennének a Seleniummal kapcsolatban, írj egy kommentet!

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!