Pavel Müller
30.6.2010

Selenium a návrhový vzor Page Objects



Selenium používáme úspěšně už několik posledních projektů. Vždycky byly automatizované testy přínosem pro kvalitu aplikace a ušetřily obrovské množství rutinní práce testerům. Představa, že lze vytvořit Selenium test tak, že se „nakliká“, a pak ho už budeme jen dokola pouštět, vezme hodně rychle za své. Je jasné, že některé části testů bude potřeba použít několikrát a že DRY princip platí i zde. Nakonec stejně nezbývá nic než použít skriptovací nebo programovací jazyk a Selenium testy udržovat jako každý jiný kód. Jak ale testy navrhovat a strukturovat? S tím jsme se nějakou dobu potýkali. Až jsem objevil návrhový vzor Page Objects.

Pro představu. Máme za úkol pokrýt testy funkčnost založení uživatele. Máme dvě poměrně jednoduché obrazovky: Seznam uživatelů:

Vytvoření nového uživatele:

Jeden test bude na pozitivní scénář založení uživatele. Další test bude na založení uživatele s existujícím uživatelským jménem. Potud je to dobré a vystačili bychom si i s naklikanými testy. Ale jak se začnou množit další scénáře vyžadující založeného uživatele, začínáme se dostávat do problémů. Editovat uživatele, smazat uživatele, zablokovat uživatele, založit požadavek s uživatelem s příslušnou rolí, atd. Původně jsme to řešili privátními metodami v rámci test třídy. To ale nestačí, pokud potřebuji např. založit uživatele v rámci jiného testu. Tak si začnu vytvářet různé helper třídy, ale v tu chvíli v tom začíná být trochu zmatek. Člověk neví, jestli daný kód napsat do helper třídy nebo přímo do testu. Nebyli jsme s tímto stavem spokojeni. Page Objects návrhový vzor to krásně řeší. Zjednodušeně vytvářím dva druhy tříd. Testy a stránky (Page Object). Page obsahuje metody, které znamenají služby, jaké stránka nabízí z pohledu uživatele. Např. tlačítka, vyplnění formuláře, odkazy, atd. Další metody umožňují zjišťovat stav, v jakém se stránka nachází. Např. počet záznamů v tabulce, přítomnost chybové hlášky, atd. Všechny tyto metody jsou implementovány pomocí Selenium API. Testy samotné pak naopak nepoužívají Selenium API vůbec. Pouze sestavují testovací scénář voláním sérií služeb na jednotlivých stránkách. A pak také obsahují veškeré asserty. Zjistí stav pomocí metody na stránce a ověří ho. Asserty nepatří do stránky a zase Selenium volání nepatří do testu. S těmito jednoduchými pravidly se dají vytvářet velmi přehledné a čitelné testy. Tady je příklad na otestování funkce založení uživatele v Orinoco platformě. Stránka odpovídající formuláři na založení uživatele:

@Page 
public class CreateUserPage extends AbstractOrinocoPage {
  @Override
  public boolean isValidPage() {
    return selenium.isElementPresent("page_user__createUser__do");
  } 
  
  public CreateUserPage fillForm(TestUser user) {
    selenium.type("username", user.getUserName());
    selenium.type("password", user.getPassword());
    selenium.type("confirmedPassword", user.getPassword());
    for (String userType : user.getUserTypes()) {
      selenium.addSelection("name=roles", "value=" + userType);
    }
    selenium.type("firstName", user.getFirstName());
    selenium.type("lastName", user.getSurname());
    selenium.type("email", user.getEmail());
    
    return this;
  }
    
  public UserListPage create() {
    clickAndWait("common_action_create_button");
    return navigateTo(UserListPage.class);
  }

Jsou tam implementovány dvě „služby“. Vyplnění formuláře a stisknutí tlačítka Create. Všimněte si, že zde se přímo používá Selenium API. Dobré bývá také pracovat s daty tak, že jsou zapouzdřené do objektů. Na jednom z projektů jsme dokonce použili přímo DTO objekty, protože už byly hotové a odpovídaly přesně formulářům. Chybí metoda pro Storno tlačítko, ale to nevadí. Doplní se, až bude potřeba pro nějaký scénář. Předek této třídy dává tušit, že bude obsahovat metody odpovídající hlavnímu menu a celému záhlaví. Takto vypadá Page objekt pro seznam uživatelů:

@Page
public class UserListPage extends AbstractOrinocoPage {
  @Override
  public boolean isValidPage() {
    return selenium.isElementPresent("page_user__listUsers__do");
  }

  public CreateUserPage createUser() {
    clickAndWait("//a[@href='createUser.do']");

    return navigateTo(CreateUserPage.class);
  }

  public TableControl getUserTable() { return getTableControl(); }
}

 Je tu implementována jenom jedna metoda na přidání nového uživatele. Za zmínku určitě stojí navigace. Každá metoda, která něco dělá s aplikací, vrací objekt typu Page. Buď vrací novou stránku při přechodu nebo sebe sama, pokud browser zůstává na stejné stránce. Metoda getUserTable() jenom zjišťuje stav na stránce. TableControl je pomocný objekt na práci s tabulkami. Ani na jedné ze stránek není jediný assert. Ten přísluší Test třídě. Test case pro správu uživatelů:

public class UserManagementTest extends AbstractOrinocoTest {
  private DashboardPage dashboardPage; 
  
  @Before
  public void login() {
    dashboardPage = loginPage.login("habele", "a");
  }
  
  @Test 
  public void createUser() {
    TestUser user = new TestUser("selAdmin", "selAdmin", Arrays.asList(TestUserType.ADMIN), "selAdmin", "selAdmin", "selAdmin@www.aspectworks.com", true);
    UserListPage userListPage = dashboardPage.openSettings().openUserManagement();
    CreateUserPage createUserPage = userListPage.createUser();
    createUserPage.fillForm(user).create();
    TableControl userTable = userListPage.getTableControl();
    TableRow row = userTable.getRow(user);
    
    assertNotNull(row);
    assertEquals(user.getUserName(), row.getCell(3));
  }
} 

Test case má standardní průběh. Příprava testovacích dat, provedení testu a kontrola výsledku. Se stránkami se velmi dobře pracuje zřetězeně právě kvůli tomu, že metody zase vrací Page objekty. Test už vůbec nepracuje se Selenium API. Vše nechává uvnitř stránek. Návrhový vzor Page Objects se velmi osvědčil. Je to přirozený návrh tříd podle uživatelského rozhraní. Testy se velmi snadno tvoří a čtou a znovupoužitelnost je velmi dobrá. Ze stránek se navíc dají tvořit další objekty vyšší úrovně zapouzdřující celé moduly. Lze pak třeba založit uživatele jedním řádkem a pokračovat ve psaní skriptu toho, co zrovna testujeme. V tomto článku nebylo zmíněno ještě několik součástí, které jsou vidět ze zdrojových kódů. Jak jsou stránky a testy konfigurovány a jak jsou tvořeny instance? Jak probíhá navigace mezi stránkami? Jak ověřit, že se browser nachází na stránce, na které je volána metoda služby? Jak funguje TableControl?
O tom zas někdy příště.

Vaše emailová adresa nebude zveřejněna

Komentáře

Děkujeme za váš komentář
Další
  • Petr Heller Kostroun

    Díky, přemýšlel jsem jak to ve své aplikaci pořešit, a tohle je přesně ono :o). Existuje taky nějaký návrhový vzor, který by řešil jak pěkně napsat JUnity např. k perzistentní vrstvě bez různých Helperů?

  • My testujeme Hibernate DAO vrstvu pomocí Spring Test a HSQL databáze. Máme to i ve školeních Spring Framework a JUnit. Nějaký náznak, jak na to, je tady: /cs/blog/2010/03/unit-testy-nad-in-memory-databazi/

  • Jan Vondrous

    Pěkný článek děkuji.

  • Pavel Janíček

    Díky za článek - já se v Seleniu snažím vyřešit, jak spravovat kód pro stránky, které jsou automaticky generované JSF frameworkem a prakticky při každém deploy se změní ID i NAME jednotlivých elementů. Řešili jste to někdo?

  • My v AspectWorks nejsme příliš fanoušci JSF, proto moc neporadím. Ideální je, dostat do formulářového elementu nějak svojí unikátní značku a na tom postavit selektory XPath.