În mod obișnuit, un test scris cu Selenium, Java și TestNG dorește să verifice corectitudinea elementelor unei pagini sau a unui modul de pe o pagină web. Abordarea clasică a acestui tip de teste o reprezintă folosirea unui număr ridicat de assert-uri, pentru compararea tuturor proprietăților dorite cu valorile așteptate ale acestora.
Această abordare are multe neajunsuri, printre care dificila mentenanță a testelor, risipa de linii de cod, lipsa de inteligibilitate. Pentru a evita aceste neajunsuri, o abordare diferită a acestui tip de teste o reprezintă conceperea testelor bazate pe compararea unor obiecte.
Să presupunem că pagina sau modulul de testat reprezintă un coș de cumpărături, afișat pe un site de cumpărături. După ce s-au făcut cumpărături, coșul conține un număr de produse. Fiecare produs, așa cum este el afișat pe pagină, conține: o etichetă (label) cu numele său, o descriere, o imagine, prețul per produs, cantitatea din acest produs care a fost pusă în coș, prețul total pentru acest produs (prețul per produs * cantitatea), și un buton prin care produsul poate fi scos din coș. Pe lângă produsele cumpărate, coșul are: o etichetă proprie, prețul total al produselor, un link pentru continuarea cumpărăturilor și un buton pentru trecerea la pasul de plată. Pagina poate conține și alte module, cum ar fi un modul lateral cu sugestii pentru cumpărături ulterioare. Acesta ar fi un modul minimal, conținând doar câteva produse, pentru care s-ar afișa doar o etichetă cu numele, o imagine și prețul său.
Coșul de cumpărături ar arăta similar celui din imaginea următoare:
După adăugarea tuturor produselor dorite, coșul este pregătit pentru a se testa dacă informațiile pe care le afișează sunt corecte: produsele afișate sunt cele care au fost cumpărate, fiecare produs există în cantitatea dorită, detaliile de plată sunt corecte, etc. .
În mod obișnuit, testul creat pentru verificarea tuturor acestor proprietăți ale coșului ar fi o înșiruire de assert-uri. Chiar dacă s-ar scrie o metodă separată de cea de test, doar pentru verificările prin assert-uri (urmând ca aceasta să fie folosită în mai multe teste), respectivele linii de cod tot ar fi dificil de menținut și nu ar fi eficient scrise. Pentru a evita scrierea acestor teste stufoase și nu foarte prietenoase, testele pot fi gândite obiectual, după cum urmează.
În primul rând, trebuie luată în vedere imaginea de ansamblu. Ce reprezintă pagina cu coșul de cumpărături? O colecție de obiecte de diverse tipuri. Urmărind structura începând de la complex la simplu, se poate observa că cel mai complex obiect (cel care le conține pe restul), este coșul de cumpărături. El are ca proprietăți: eticheta, lista de produse cumpărate, un link, un buton și un modul lateral. Dintre aceste proprietăți, eticheta este reprezentată în Java de un String, din punct de vedere al testului. Prețul poate fi reprezentat de asemenea de un String. Lista produselor reprezintă de fapt o listă conținând obiecte de tipul "produs". Linkul afișat este și el un obiect, la fel și butonul sau modulul lateral.
În urma identificării obiectelor de la cel mai de sus nivel, se poate descrie obiectul "ShoppingCart" după cum urmează:
public class ShoppingCart {
private String title;
private List productList;
private String totalPrice;
private Button paymentButton;
private Link shopMoreLink;
private SuggestionsModule suggestionsModule;
}
Analizând în continuare în adâncime structura obiectelor, se observă că: un produs conține (are ca proprietăți) - o "imagine" (adică un alt tip de obiect), o etichetă care-i reprezintă numele (un String), un text descriptiv (un alt String), prețul per obiect (un String), cantitatea (un String), prețul total al produsului (un String), și un buton (care reprezintă un obiect care a fost deja menționat ca proprietate a coșului). Reprezentarea obiectuală a produsului poate fi făcută, conform analizei, după cum urmează:
public class Product {
private String productLabel;
private String productDescription;
private Image image;
private String pricePerItem;
private String quantity;
private String totalPricePerProduct;
private Button removeButton;
}
Linkul menționat în cadrului coșului poate conține, ca un set minimal de proprietăți, o etichetă (un String, textul pe care-l vede userul afișat) și URL-ul care se deschide prin apăsarea linkului (un String). Reprezentarea sa obiectuală poate fi făcută astfel:
public class Link {
private String linkLabel;
private String linkURL;
}
După această logică, se pot identifica toate proprietățile tuturor obiectelor afișate pe pagină și se pot construi aceste obiecte, ,,spărgându"-le până se descompun în proprietăți care sunt obiecte sau primitive Java.
După terminarea structurării coșului de cumpărături în obiecte, trebuie înțeles cum vor fi folosite acestea în teste. Prima parte a testului (sau partea care se realizează înaintea testului) o reprezintă adăugarea produselor în coș. Testul trebuie doar să verifice că în coș se află produsele corecte.
Pentru a construi obiectul "coș de cumpărături" la care se așteaptă testul (conținutul "expected"), trebuie creat constructorul care atribuie tuturor proprietăților sale valori de tipul proprietăților respective. Astfel, se pasează constructorului parametri care corespund proprietăților obiectului, având tipul acestor proprietăți (de exemplu pentru o proprietate de tip String se pasează în constructor un String, pentru un int se pasează un parametru int).
începând de la cel mai simplu obiect, se creează constructorii. Pentru Link:
public Link(String linkLabel, String linkURL) {
this.linkLabel = linkLabel;
this.linkURL = linkURL;
}
Aici se poate observa că - obiectul Link are o etichetă și un URL, ambele de tip String, a căror valoarea se instanțiază cu valorile primite din parametrii constructorului.
Pentru produs se generează următorul constructor:
public Product(String productLabel, String productDescription, Image image, String pricePerItem, String quantity, String totalPricePerProduct, Button removeButton) {
this.productLabel = productLabel;
this.productDescription = productDescription;
this.image = image;
this.pricePerItem = pricePerItem;
this.quantity = quantity;
this.totalPricePerProduct = totalPricePerProduct;
this.removeButton = removeButton;
}
Pentru coșul de cumpărături constructorul are forma:
public ShoppingCart(String title, List productList, String totalPrice, Button paymentButton, Link shopMoreLink, SuggestionsModule suggestionsModule) {
this.title = title;
this.productList = productList;
this.totalPrice = totalPrice;
this.paymentButton = paymentButton;
this.shopMoreLink = shopMoreLink;
this.suggestionsModule = suggestionsModule;
}
Pe baza constructorului, se vor genera următoarele obiecte (cele car vor servi drept "expected"), pasând constructorului niște valori având tipul parametrilor cărora li se vor atribui acestea:
public static final Link CONTINUE_SHOPPING_LINK =
new Link("Continue shopping",
"http://continue.shopping.com");
public static final Button GO_TO_PAYMENT_BUTTON =
new Button("Proceed to payment",
"http://some.url.com");
public static final Product LATTE_MACHIATTO_2 =
new Product("Latte Machiato",
"Classic latte machiato with a dash of cocoa on top",
Image.IMAGE_LATTE_MACHIATO,
"5 Ron",
"2",
"10 RON",
Button.REMOVE_PRODUCT);
→ aici, obiectele de tip imagine și buton au fost construite folosind constructorii specifici acelor obiecte
public static final Product CHOCO_FRAPPE_3 =
new Product("Choco-whip Frappe",
"Frappe with a twist of whipped cream and chocolate syrup",
Image.IMAGE_CHOCO_FRAPPE,
"5 Ron",
"3",
"15 RON",
Button.REMOVE_PRODUCT);
→ aici, obiectele de tip imagine și buton au fost construite folosind constructorii specifici acelor obiecte
public static final Product CHOCO_FRAPPE_3 =
new Product("Choco-whip Frappe",
"Frappe with a twist of whipped cream and chocolate syrup",
Image.IMAGE_CHOCO_FRAPPE,
"5 Ron",
"3",
"15 RON",
Button.REMOVE_PRODUCT);
→ aici, obiectele de tip imagine și buton au fost construite folosind constructorii specifici acelor obiecte
public static final ShoppingCart SHOPPING_CART =
new ShoppingCart("My Shopping Cart",
ImmutableList.of(Product.LATTE_MACHIATTO_2,
Product.CHOCO_FRAPPE_3,
Product.CARAMEL_MOCCACHINO_1),
"30 RON",
Button.GO_TO_PAYMENT_BUTTON,
Link.CONTINUE_SHOPPING_LINK,
SuggestionsModule.SUGGESTIONS_MODULE);
→ aici, obiectul de tip "modul de sugestie" a fost construit folosind constructorul său specific, iar lista de produse, butonul și linkul au fost exemplificate deasupra liniei în care se construiește obiectul "coș de cumpărături".
Construirea conținutului "actual"
Pentru construirea obiectului "actual", adică pentru citirea proprietăților obiectelor direct de pe pagina unde acestea sunt afișate, se va crea un nou constructor în fiecare obiect, care ia ca parametri fie un webElement, fie o listă de webElemente, atâtea câte sunt necesare pentru generarea proprietăților obiectului. WebElementele reprezintă descrierea elementelor HTML în formatul specific Selenium.
Ca exemplu, pentru obiectul link: cele două proprietăți, eticheta și URL-ul asociate se pot deduce dintr-un singur webElement. Un element de tip link este reprezentat din punct de vedere al HTML-ului, ca un tag , având un atribut "href" (prin extragerea căruia se identifică URL-ul). Apelând metoda getText() a librăriei de Selenium direct pe elementul "a", se obține valoarea etichetei. Astfel, constructorul bazat pe webElement este descris mai jos, și instanțiază proprietățile obiectului extrăgându-le din elementul HTML corespunzător:
public Link(WebElement element) {
this.linkLabel = element.getText();
this.linkURL = element.getAttribute("href");
}
Pentru construirea obiectului "actual" corespunzător unui produs, în funcție de câte webElemente sunt necesare pentru obținerea tuturor proprietăților, se va defini un constructor care să ia ca parametru fie un webElement, fie o listă de webElemente. Presupunând că se folosește un singur element, constructorul va arăta astfel (ca exemplu):
public Product(WebElement element) {
this.productLabel = element.findElement(
By.cssSelector(someSelectorHere)).getText();
this.productDescription = element.findElement(
By.cssSelector(someOtherSelectorHere)).getText();
this.image = new Image(element);
this.pricePerItem = element.findElement(
By.cssSelector(anotherSelectorHere)).getText();
this.quantity = element.findElement(
By.cssSelector(yetAnotherSelectorHere)).getText();
this.totalPricePerProduct = element.findElement(
By.cssSelector(aSelectorHere)).getText();
this.removeButton = new Button(element);
}
Se poate observa că în cazul produsului, pentru generarea unor proprietăți s-au apelat constructorii obiectelor de tipul corespunzător, mai exact constructorii care iau ca parametri tot webElemente. Practic, orice constructor bazat pe webElemente apelează doar constructori cu parametri de tip webElemente pentru inițializarea proprietăților sale. Proprietățile rămase se instanțiază în funcție de parametrul dat constructorului. De exemplu, pentru etichetă (productLabel), se apelează metoda din Selenium getText() pe un element relativ la elementul care este pasat în constructor.
O dată cu definirea tuturor constructorilor bazați pe webElemente, se poate genera și constructorul pentru cel mai complex dintre ele, coșul de cumpărături:
public ShoppingCart(List webElementList) {
this.title = webElementList.get(0).getText;
this.productList = productList;
this.totalPrice = webElementList.get(2).getText;
this.paymentButton = new Button(webElementList.get(3));
this.shopMoreLink = new Link(webElementList.get(4));
this.suggestionsModule = suggestionsModule;
}
În urma definirii obiectelor și constructorilor, se trece la pasul de scriere a testelor. Cerința testului era de a compara coșul de cumpărături cu unul "expected", adică compararea tuturor proprietăților coșului de cumpărături cu cele ale coșului așteptat. Aceste proprietăți sunt la rândul lor obiecte, deci și proprietățile lor trebuie comparate cu proprietățile unor obiecte așteptate. Deoarece valorile expected s-au construit prin intermediul primului tip de constructor (cel cu parametri având tipul proprietăților care se instanțiază) și avem un constructor pentru generarea conținutului "actual" (prin interpretarea proprietăților unor webElemente), testul care trebuie scris conține un singur assert. Acesta va compara proprietățile "expected" cu cele "actual" prin simpla comparare a celor două obiecte. Ar trebui menționat că, o dată cu definirea obiectelor, trebuie implementată în cadrul fiecăruia și metoda de equals() (cea care verifică dacă două obiecte sunt egale).
Astfel, testul poate fi scris după cum urmează:
@Test
public void checkMyShoppingCart() {
assertEquals(new ShoppingCart(theListOfWebElements),
SHOPPING_CART,
"There was an incorrect value in the shopping cart");
}
Bineînțeles, acest test nu descrie pașii necesari construirii coșului de cumpărături (navigarea pe site-ul de cumpărături și adăugarea produselor). Acești pași pot fi făcuți în cadrul testului dacă e necesar, sau în metode de tip "@Before".
În cazul în care se dorește, de exemplu, testarea aceluiași conținut dar pe limbi diferite, se poate pasa un dataProvider testului, în care să se țină un parametru folosit în test pentru schimbarea limbii, precum și valoarea "expected" a coșului pentru limba respectivă. În acest caz, testul va deveni următorul:
@Test(dataProvider = "theDataProvider")
public void checkMyShoppingCart(
String theLanguage,
ShoppingCart actualShoppingCartValuePerLanguage) {
changeTheLanguageOnTheSite(theLanguage);
assertEquals(new ShoppingCart(
theListOfWebElements),
actualShoppingCartValuePerLanguage,
"There was an incorrect value in the shopping cart");
}
}
Dataprovider-ul folosit în acest test va arăta ca în exemplul de mai jos:
@DataProvider
public Object[][] theDataProvider() {
return new Object[][]{
{ "english", SHOPPING_CART},
{ "german", SHOPPING_CART_GERMAN },
{ "spanish", SHOPPING_CART_SPANISH}
};
}
Astfel, dacă se presupune că site-ul de cumpărături este disponibil în 20 de limbi, testul care verifică afișarea corectă a coșului de cumpărături tradus va avea un număr redus de linii de cod și va fi scris o singură dată, fiind însă rulat pe toate limbile existente.
Modul de scriere a testelor prin compararea obiectelor generate din webElemente cu cele generate din obiecte și primitive are numeroase beneficii. În primul rând, testul este unul foarte scurt, cu un scop bine determinat, făcând un singur lucru: compararea valorilor "actual" cu cele "expected". Testul în sine nu necesită multă mentenanță, deoarece în urma schimbărilor în pagina accesată de user-i, trebuie schimbat nu testul, ci modul în care se generează proprietățile comparate. Schimbarea valorii unui label de pe pagină necesită doar schimbarea valorii "expected" corespunzătoare unui obiect, această schimbare făcându-se într-un singur loc, dar de ea beneficiind un număr mare de teste. Astfel se separă partea de test de partea de generare a valorilor așteptate.
Un alt beneficiu îl reprezintă structura compactă a testului, nefiind necesară scrierea unui număr ridicat de assert-uri, sau pasarea unui număr mare de parametri unei metode care trebuie să verifice acele valori. În locul acelor mulți parametri, se pasează direct obiectul care conține proprietățile de comparat.