JUnit 5 i Selenium - klasa PageFactory z pakietu support biblioteki Selenium

Biblioteka Selenium dostarcza klasę PageFactory, która upraszcza implementację wzorca Page Object. Klasa PageFactory pozwala na utworzenie nowej instacji obiektu dowolnej klasy, która deklaruje pola typu WebElement lub List<WebElement> oznaczone adnotacją @FindBy lub posiadające nazwy takie jak atrubut id lub name elementu HTML na stronie. Skuteczność i prostotę tego rozwiązania pokażę na przykładzie testów aplikacji TodoMVC.

O serii

Czytasz część 5 serii artykułów na temat automatyzacji testów aplikacji internetowej TodoMVC. Pozostałe części:

Kod źródłowy

Kod źródłowy opracowany w poprzednim artykule znajduje się w repozytorium Git: https://gitlab.com/qalabs/blog/junit5-selenium-todomvc-example w branchu 4-configuration. Zmiany opisywane w tym artykule znajdują się w pakiecie pl.qalabs.blog.junit5.selenium.todomvc_with_page_factory projektu, w branchu 5-selenium-page-factory.

Wprowadzenie do PageFactory

@FindBy

Jak wspominałem na początku artykułu, klasa PageFactory pozwala na utworzenie nowej instacji obiektu dowolnej klasy, która deklaruje pola typu WebElement lub List<WebElement>. Poniższy przykład to uproszczona wersja klasy TodoMvc posiadająca pola newTodoInput, todoCount oraz todos, które zostały oznaczone adnotacją @FindBy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class TodoMvc {

@FindBy(className = "new-todo")
private WebElement newTodoInput;

@FindBy(css = ".todo-count > strong")
private WebElement todoCount;

@FindBy(css = ".todo-list li")
private List<WebElement> todos;

private final WebDriver driver;

public TodoMvc(WebDriver driver) {
this.driver = driver;
}

public void navigateTo() {
driver.get("http://todomvc.com/examples/vanillajs");
}

public void createTodo(String todoName) {
newTodoInput.sendKeys(todoName + Keys.ENTER);
}

public int getTodoCount() {
return todos.size();
}

public int getTodosLeft() {
return Integer.parseInt(todoCount.getText());
}

public boolean todoExists(String todoName) {
return getTodos().stream().anyMatch(todoName::equals);
}

public List<String> getTodos() {
return todos
.stream()
.map(WebElement::getText)
.collect(Collectors.toList());
}
}

@FindBys, @FindAll

@FindBy to nie jedyna adnotacja, której możemy użyć do lokalizowania elementów. Pozostałe adnotacje, których możemy używać do oznaczania pól to @FindBys i @FindAll.

  • @FindBys

Adnotacja @FindBys wykorzystywana jest do oznaczenia pól, dla których wykonana zostanie seria wyszukiwań, jedno po drugim:

1
2
3
4
5
@FindBys({
@FindBy(id = "menu"),
@FindBy(className = "button")
})
private WebElement element;

W powyższym przykładzie, wyszukany zostanie pierwszy element posiadający atrybut class = "button", który występuje w elemencie z atrybutem id = "menu".

  • @FindAll

Adnotacja @FindAll wykorzystywana jest do wyszukiwania wszystkich elementów spełniających jedno z zadanych kryteriów:

1
2
3
4
5
@FindAll({
@FindBy(id = "menu"),
@FindBy(className = "button")
})
private List<WebElement> webElements;

W powyższym przykładzie, wyszukane zostaną wszystkie elementy, posiadające atrybut class = "button" oraz wszystkie elementy posiadające id = "menu". Kolejność zwróconych elementów nie musi być taka w jakiej występują one w dokumencie.

PageFactory

W poniższym przykładzie zaimplementowałem test tworzenia zadania, wykorzystujący wcześniej opisaną klasę TodoMvc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@DisplayName("Adding Todo")
class AddTodoTest {

private WebDriver driver;
private TodoMvc page;

@BeforeAll
static void beforeAll() {
WebDriverManager.chromedriver().setup();
}

@BeforeEach
void beforeEach() {
driver = new ChromeDriver();
page = PageFactory.initElements(driver, TodoMvc.class);
page.navigateTo();
}

@AfterEach
void afterEach() {
driver.quit();
}

@Test
@DisplayName("Creates Todo with given name")
void createsTodo() {
var todoName = "Todo 1";

page.createTodo(todoName);
assertEquals(1, page.getTodosLeft());
assertTrue(page.todoExists(todoName));
}
}

Obiekt klasy TodoMvc tworzony jest przez PageFactory, przed każdym testem, w metodzie beforeEach: page = PageFactory.initElements(driver, TodoMvc.class);.

W pierwszym kroku metoda PageFactory.initElements(driver, TodoMvc.class) utworzy instancję obiektu klasy TodoMvc z wykorzystaniem mechanizmu refleksji w Javie. W kolejnym kroku zainicjalizowane zostaną pola oznaczone adnotacją @FindBy (lub @FindAll i @FindBys). Użycie metody PageFactory.initElements(driver, TodoMvc.class) wymaga również, aby klasa TodoMvc posiadała konstruktor przyjmujący parametr typu WebDriver.

Tip: Klasa PageFactory posiada również inne metody inicjalizujące. Najbardziej iteresująca jest metoda initElements(WebDriver driver, Object page), która jako drugi parametr przyjmuje wcześniej utworzony Page Object. Metoda ta przyda się szczególnie w przypadku, gdy nasze obiekty wymagają bardziej skomplikowanej inicjalizacji.

Wyszukiwanie elementów

Jak wspominałem, podczas inicjalizacji obiektu TodoMvc nie zostaną wykonane żadne zapytania o elementy w DOM. Właściwe wyszukiwanie elementu zostanie wykonane każdorazowo podczas dostępu do danego pola. Kiedy zatem wykonamy np. instrukcję newTodoInput.sendKeys(todoName + Keys.ENTER)) w metodzie createTodo, Selenium zadba o wyszukanie elementu o klasie todo-input. Sprowadzi się to zatem do wykonania instrukcji driver.findElement(By.className('new-todo')).sendKeys(todoName + Keys.ENTER). Wynika to z deklaracji pola newTodoInput: @FindBy(className = "new-todo"). Możemy się zatem spodziewać, że potencjalny wyjątek wynikający np. z nieodnalezienia elementu w DOM zostanie wyrzucony przy próbie dostępu do pola, a nie podczas inicjalizcji obiektu.

Tip: Wzorzec projektowy wykorzystywany przez Selenium w celu osiągnięcia powyższego efektu to wzorzec Proxy: https://en.wikipedia.org/wiki/Proxy_pattern#Java

@CacheLookup

Zdarza się jednak, że nie ma potrzeby wykonywania wyszukiwania elementu za każdym razem, gdy potrzebujemy dostępu do jakiegoś pola. W takim wypadku możemy użyć adnotacji @CacheLookup, która poinstruuje Selenium, że element nie zmienia się po pierwszym wyszukaniu i nie chcemy go ponawiać.

W moim przykładzie pole typu input jest niezmienne, zatem mogę oznaczyć je adnotacją @CacheLookup:

1
2
3
4
5
6
7
public class TodoMvc {

@FindBy(className = "new-todo")
@CacheLookup
private WebElement newTodoInput;

}

Od teraz tyko pierwszy dostęp do pola newTodoInput będzie wykonywał właściwe wyszukanie elementu w DOM.

Tip: Wyjątek typu org.openqa.selenium.StaleElementReferenceException: stale element reference: element is not attached to the page document może sygnalizować niepoprawne użycie adnotacji. Warto się upewnić, że pole jest niezmienne w cyklu życia obiektu strony!

Uruchomienie testów

Nadszedł czas na przetestowanie zmian w projekcie. W tym celu uruchom wiersz poleceń i w katalogu projektu wykonaj polecenie:

./gradlew clean test --tests *with_page_factory.tests.AddTodoTest

Powyższe polecenie wykona testy z klasy AddTodoTest z pakietu pl.qalabs.blog.junit5.selenium.todomvc_with_page_factory.tests.

Tip: Opcja tests pozwala na zaawansowane wybieranie testów do uruchomienia. Więcej informacji o wybieraniu testów do uruchomienia znajdziesz w oficjalnej dokumentacji Gradle: https://docs.gradle.org/current/userguide/java_testing.html#test_filtering

Repozytorium Git projektu

Kod źródłowy opracowany w artykule znajduje się w repozytorium Git: https://gitlab.com/qalabs/blog/junit5-selenium-todomvc-example w pakiecie pl.qalabs.blog.junit5.selenium.todomvc_with_page_factory, w branchu 5-selenium-page-factory.

Podsumowanie

Klasa PageFactory, dostarczana przez biblitekę Selenium, znacznie upraszcza implementację wzorca Page Object dostarczając alternatywny mechanizm wyszukiwania elementów za pomocą adnotacji @FindBy, @FindBys czy w końcu @FindAll. Dzięki użyciu adnotacji, kod staje się znacznie czytelniejszy a programista pozbywa się sporo niepotrzebnego i powtarzającego się kodu, co ostatecznie doprowadzi do łatwiejszego jego utrzymania.

Zobacz również

Rafał Borowiec

Nazywam się Rafał Borowiec. Jestem w branży IT od ponad 10 lat, przygodę rozpoczynałem jako tester oprogramowania. Oprócz testowania oprogramowania i zapewniania jakości, specjalizuję się w wytwarzaniu oprogramowania oraz zarządzaniu projektami i zespołami. Chętnie dzielę się wiedzą, prowadzę blog dotyczący programowania, jestem wykładowcą oraz trenerem IT.