ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 149
Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 24
Abonament PDF

Modelarea obiectuală a testelor de Selenium

Corina Pip
Senior QA Engineer
@Betfair



TESTARE


Î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.

Studiu de caz

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ă.

Soluția

Analiza

Î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.

Construirea conținutului "expected"

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:

  • linkul de continuare a cumpărăturilor:
public 	static final Link CONTINUE_SHOPPING_LINK = 
  new Link("Continue 	shopping", 
   "http://continue.shopping.com");

  • butonul pentru avansarea la faza plății:
public 	static final Button GO_TO_PAYMENT_BUTTON = 
  new Button("Proceed 	to payment", 
   "http://some.url.com");

  • produsul 1 din coș:
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

  • produsul 2 din coș:
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

  • produsul 3 din coș:
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

  • coșul de cumpărături:
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;
}

Testul

Î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.

Beneficii

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.

În aceeaşi ediţie ... (24)

▼ TOATE ARTICOLELE ▼

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects