Ten wpis jest rozszerzeniem maila, którego wysłałam do testelkowej listy mailingowej. Jeżeli chcesz otrzymywać na swoją skrzynkę treści merytoryczne dotyczące Selenium, a także „miękkie” dotyczące testowania i pracy w zawodzie testera w ogóle, zapisz się tutaj.
Metody findElement i findElements w Javie (oraz FindElement i FindElements w C#), to takie metody co to wszystkim się wydaje, że wiedzą jak działają i że są proste jak budowa cepa.
No chyba, że nie. Wszystko jest proste, gdy to znamy i rozumiemy. A z tymi metodami jest tak, że mogą się wydawać oczywiste w działaniu i dlatego nie zagłębiamy się w ten temat. Widzę to po pytaniach jakie do mnie spływają. Zresztą, też mi się wydawało kiedyś, że wiem jak te metody działają (nie, nie wiedziałam).
Spis treści
Podstawowa różnica
Podstawowa różnica jest w miarę zrozumiała, ale jej konsekwencje już niekoniecznie. Chodzi o to co zwraca jedna i druga metoda. Metoda findElement w Javie zwraca obiekt typu WebElement. Analogicznie metoda FindElement w C# zwraca obiekt implementujący interfejs IWebElement. Czyli mówiąc po ludzku ta metoda zwraca nam jeden element.
//Java WebElement element = driver.findElement(By.id("jakieś-id"));
//C# IWebElement element = driver.FindElement(By.Id("jakieś-id"));
Metoda findElements (FindElements) z kolei jak sama nazwa wskazuje zwraca nam wiele elementów (o ile je znajdzie). W C# zwróci nam kolekcję elementów, a w Javie listę.
//Java List<WebElement> elements = driver.findElements(By.cssSelector(".jakaśklasa"));
//C# IList<IWebElement> elements = driver.FindElements(By.CssSelector(".jakaśklasa"));
To wydaje się proste, ale teraz zobacz jakie są tego konsekwencje.
Maksymalnie jeden element
Metoda findElement (FindElement w C#) zawsze zwróci maksymalnie jeden element. Oznacza to, że jeżeli w parametrze tej metody przekażesz lokator, który zaznaczy np. pięć elementów, to ta metoda nadal zwróci tylko jeden element. Który? Pierwszy, który znajdzie, czyli najwyżej w drzewie DOM.
Weźmy na przykład poniższy fragment HTMLa jakiejś hipotetycznej strony.
<div> <table> <tr classs="row" id="subtotal"> <td></td> <td></td> </tr> <tr classs="row" id="tax"> <td></td> <td></td> </tr> <tr classs="row" id="total"> <td></td> <td></td> </tr> </table> </div>
Załóżmy, że chcemy zaznaczyć trzeci wiersz (<tr classs=”row” id=”total”>). Załóżmy też, że jesteśmy nowi w lokatorach albo po prostu nie zauważyliśmy, że tą samą klasę ma jeszcze kilka innych elementów i zbudujemy dla tego elementu poniższy lokator:
//Java WebElement row = driver.findElement(By.cssSelector(".row"));
//C# IWebElement row = driver.FindElements(By.CssSelector(".row"));
Który element wtedy dostaniemy? Pierwszy element, jaki driver znajdzie w DOMie strony odpowiadający podanemu lokatorowi, a więc pierwszy wiersz.
Mały offtop: powinniśmy unikać takiej sytuacji. Jeżeli chcemy namierzyć jeden konkretny element, to nasz lokator powinien wskazywać tylko na jeden element, żeby uniknąć nieprzewidzianego zachowania testu w przyszłości, gdy np. zmieni się kolejność elementów w drzewie DOM. To co napisaliśmy rzeczywiście zadziała teraz do pobrania pierwszego wiersza, więc możecie chcieć powiedzieć, że przecież jeżeli chcemy dostać pierwszy wiersz to działa. No działa, ale wejdzie dodatkowy wiersz na samą górę o tej samej klasie i testy się sypią (chociaż nie powinny).
Metoda findElements (FindElements) zwróci nam natomiast tyle elementów, ile znajdzie za pomocą podanego lokatora.
A skoro findElements zwraca wiele elementów to…
To nie można zrobić czegoś takiego:
//Java List<WebElement> elements = driver.findElements(By.cssSelector(".jakaśklasa")); elements.click();
//C# IList<IWebElement> elements = driver.FindElements(By.CssSelector(".jakaśklasa")); elements.Click();
Metoda click() (Click()) zadziała na pojedynczym elemencie, dlatego najpierw musimy wyciągnąć z listy to, czego potrzebujemy, a dopiero później klikać czy wykonywać inne akcje na poszczególnych elementach.
//Java List<WebElement> elements = driver.findElements(By.cssSelector(".jakaśklasa")); elements.get(0).click();
//C# IList<IWebElement> elements = driver.FindElements(By.CssSelector(".jakaśklasa")); elements[0].Click();
To jest wbrew pozorom bardzo częsty błąd wynikający z chęci wykonania jakiejś akcji na każdym elemencie listy. Jeżeli chcemy coś takiego zrobić dla każdego elementu listy, to opcja prostsza w zrozumienia to np. pętla foreach, a trudniejsza: zapytania LINQ (C#) albo Stream API (Java).
A co gdy te metody nic nie znajdą?
To też jest zabawna sprawa. W przypadku metody findElements… nic. Po prostu dostaniemy pustą kolekcję lub listę. Natomiast findElement rzuci nam wyjątkiem: NoSuchElementException.
Dlaczego w ogóle ta różnica nas interesuje? Bo gdy dostaniemy wyjątek, test się zatrzyma, a w przypadku findElements nigdy to nie nastąpi (a na pewno nie dlatego, że driver nie znalazł elementu). To sprawia, że test się zachowa zupełnie inaczej. Ta wiedza może się nam też przydać do budowania asercji. Przykład: chcesz potwierdzić, że danego elementu nie ma w DOMie. Jak to zrobić? Jedną opcją jest użycie findElements i potwierdzenie że lista (kolekcja jest pusta). Musisz się tylko najpierw jakoś upewnić, że strona się na pewno załadowała i że brak elementów nie wynika z tego, że po prostu jeszcze się nie pojawiły. Druga opcja to użycie findElement i opakowanie tego w asercję, która potwierdzi, że wyjątek został rzucony. JUnit 5 (Java) ma do tego asercję assertThrows(), a NUnit (C#) Throws(). Osobiście preferuję właśnie to rozwiązanie, bo mam poczucie większej kontroli nad tym jak test, aczkolwiek może to być trochę trudne dla początkujących.
Niejawne czekanie (Implicit Wait)
Implicit Wait to taki globalny timeout, który z reguły ustawiamy na driverze gdzieś przed testem. Dzięki niemu możemy powiedzieć, żeby driver próbował znaleźć element przez 5 sekund, a jeżeli go nie znajdzie to rzuci nam wyjątek. W ten sposób zabezpieczamy się przed sytuacją, w której driver próbuje namierzyć element zanim ten ma szansę się załadować.
Taki timeout zadziała na metodach findElement i findElements. Jak możesz się domyślać, w obu przypadkach zadziała trochę inaczej. W przypadku findElement sprawa jest bardziej intuicyjna. Driver będzie po prostu próbował znaleźć ten element i jak go nie znajdzie przez ustalony czas to rzuci wyjątkiem. Ale w przypadku findElements driver będzie czekał na element przez maksymalnie 5 sekund i jeżeli znajdzie przynajmniej jeden, to zwróci nam to co znalazł. To oznacza w praktyce, że możliwy jest scenariusz, w którym mimo, że mamy ustawiony timeout, dostaniemy mniej elementów niż oczekujemy, ponieważ kolejne elementy się nie załadowały. Czyli przykładowo chcemy znaleźć wszystkie trzy wiersze z już wspomnianego przykładu, ale z jakiegoś powodu nie ładują się one na raz. Wtedy mimo ustawienia timeout’u, driver poczeka na pierwszy element jaki mu pasuje do lokatora i zwróci go nie czekając na pozostałe. Dostaniemy zatem listę jednoelementową, zamiast spodziewanych trzech elementów.
Czy znałeś lub znałaś te wszystkie niuanse? Podziel się swoimi spostrzeżeniami w komentarzu!