JUnit 5 i Selenium - Wprowadzenie do projektu automatycznych testów aplikacji internetowej

Po artykule opisującym podstawowe funkcjonalności frameworka JUnit 5 oraz serii trzech artykułów na temat konfiguracji środowiska na potrzeby testowania aplikacji internetowych, przyszedł wreszcie czas na zmierzenie się z automatyzacją testów prawdziwej aplikacji. Zaprezentuję praktyczne połączenie elementów dostarczanych przez technologię Java, framework JUnit 5, narzędzie automatyzacji procesu budowania aplikacji Gradle i bibliotekę Selenium WebDriver w rzeczywistym projekcie. Zacznę od podstaw, w kolejnych tekstach stopniowo wprowadzając bardziej zaawansowane koncepcje, tworząc tym samym zestaw profesjonalnych testów automatycznych.

O serii

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

Przygotowanie środowiska - niezbędne kroki

Aby w pełni przygotować środowisko, będziesz potrzebować:

  • Java SE Development Kit, w wersji conajmniej 9 (zalecam najnowszą wersję),
  • IntelliJ, darmowa wersja Community będzie wystarczająca do tworzenia nawet zaawansowanych projektów,
  • Google Chrome i Mozilla Firefox w najnowszej wersji lub ich odpowiedniki portable (dla systemu Windows),
  • Sterowniki do wybranych przeglądarek, w najnowszej wersji.

Szczegółowe kroki przygotowania środowiska w systemie Windows zostały opisane w serii artykułów:

Jeśli używasz systemu macOS, możesz skorzystać z porad przygotowania środowiska zebranych w tym artykule:

Konfiguracja projektu

Pobranie szablonu projektu i pierwsze uruchomienie

Aby uprościć proces tworzenia i uruchomienia projektu, przygotowałem gotowy do użycia szablon projektu, dostępny na GitLab.

Przygotuj katalogu projektu i zainicjuj repozytorium Git:

mkdir my-selenium-project
cd my-selenium-project
git init

Przygotuj własny projekt na podstawie szablonu (fork):

git remote add upstream https://gitlab.com/qalabs/blog/junit5-selenium-gradle-template.git
git pull -s recursive -X theirs upstream master

Uruchom testy:

gradlew clean test

Polecenie uruchomi testy z użyciem Gradle, pobierając wcześniej niezbędne zależności oraz kompilując kod projektu. Jeśli wszystko zostało poprawnie skonfigurowane, wykonają się dwa testy otwierające stronę https://qalabs.pl w przeglądarkach Google Chrome i Mozilla Firefox. W konsoli powinieneś zobaczyć:

1
2
3
4
5
6
7
8
9
10
11
λ gradle clean test

> Task :test

pl.qalabs.blog.junit5.selenium.HelloSeleniumChromeTest > helloSelenium() PASSED

pl.qalabs.blog.junit5.selenium.HelloSeleniumFirefoxTest > helloSelenium() PASSED


BUILD SUCCESSFUL in 12s
4 actionable tasks: 4 executed

Jeżeli zamiast powyższych komunikatów w konsoli widzisz błędy, wróć do wcześniejszych artykułów na temat przygotowania środowiska i upewnij się, że zostało ono skonfigurowane prawidłowo. W szczególności upewnij się, że:

  • Posiadasz Java SE Development Kit w wersji conajmniej 9,
  • Zainstalowałeś przeglądarki internetowe Google Chrome i Mozilla Firefox,
  • Zainstalowałeś sterowniki do powyższych przeglądarek i są one dostępne z poziomu zmiennej systemowej PATH.

Tip: junit5-selenium-gradle-template został stworzony na podstawie innego projektu, który opisywałem w artykule JUnit 5 - Pierwsze kroki.

Import projektu do IntelliJ

Aby zaimportować projekt w IntelliJ, wybierz opcję File | Open i wskaż plik gradle.build. Otwórz plik jako projekt i poczekaj chwilę, aż zaimportuje się wraz ze wszystkimi zależnościami.

Konfiguracja zależności (ang. dependencies) projektu znajduje się w pliku build.gradle. W projekcie, który właśnie przygotowałeś, zdefiniowane są zależności JUnit 5 oraz Selenium WebDriver:

1
2
3
4
5
6
7
8
9
dependencies {
/* JUnit 5 */
testCompile("org.junit.jupiter:junit-jupiter-api:5.1.1")
testCompile("org.junit.jupiter:junit-jupiter-engine:5.1.1")
/* Optional dependency for parameterized tests */
testCompile("org.junit.jupiter:junit-jupiter-params:5.1.1")
/* Selenium */
testCompile("org.seleniumhq.selenium:selenium-java:3.9.1")
}

Tip: Jeżeli nie pracowałeś wcześniej z IntelliJ, w artykule Narzędzia - Java, Gradle i IntelliJ znajdziesz linki do dokumentacji, które pomogą ci rozpocząć pracę z tym środowiskiem.

Pierwsze testy aplikacji internetowej

Naszym testowanym systemem (ang. system under test, SUT) będzie prosta aplikacja Todo, umożliwiająca zarządzanie listą zadań do wykonania. Skorzystamy z TodoMVC, czyli strony z przykładowymi implementacjami takiej aplikacji, napisanymi w różnych frameworkach JavaScript. Użyjemy implementacji Todo w Vanilla JS (inaczej “czystym” JavaScript), którą znajdziesz pod adresem http://todomvc.com/examples/vanillajs.

Nasza testowana aplikacja jest aplikacją typu Single Page Application, czyli między innymi nie przeładowuje strony po wykonaniu akcji przez użytkownika. Do przechowywania listy zadań do wykonania wykorzystuje Local Storage.
Scenariusze testowe, które zostaną zautomatyzowane:

  • Utworzenie zadania
  • Edycja zadania
  • Usunięcie zadania
  • Oznaczenie zadania jako zakończonego
  • Oznaczenie wielu zadań jako zakończonych
  • Usunięcie zakończonych zadań

Tip: Zanim rozpoczniesz automatyzację, zapoznaj się dobrze z testowaną aplikacją. Przemyśl powyższe scenariusze, również pod kątem analizy DOM (Document Object Model).

Utworzenie klasy testowej

Zacznijmy od utworzenia klasy testowej, która będzie zawierała metody testowe.

  • W katalogu testów (src/test/java), utwórz nowy pakiet (package) o nazwie pl.qalabs.blog.junit5.selenium.todomvc
  • Dodaj w nim klasę o nazwie TodoMvcTests o następującej zawartości:
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
45
46
47
48
49
50
51
52
53
54
55
56
package pl.qalabs.blog.junit5.selenium.todomvc;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("Managing Todos")
class TodoMvcTests {

@BeforeEach
void beforeEach() {

}

@AfterEach
void afterEach() {

}

@Test
@DisplayName("Creates Todo with given title")
void createsTodo() {

}

@Test
@DisplayName("Edits inline double-clicked Todo")
void editsTodo() {

}

@Test
@DisplayName("Removes selected Todo")
void removesTodo() {

}

@Test
@DisplayName("Toggles selected Todo as completed")
void togglesTodoAsCompleted() {

}

@Test
@DisplayName("Toggles all Todos as completed")
void togglesAllTodosAsCompleted() {

}

@Test
@DisplayName("Clears all completed Todos")
void clearsCompletedTodos() {

}
}

Uruchamianie testów

Tak utworzony test możesz uruchomić bezpośrednio z IntelliJ, który wspiera JUnit 5 natywnie, czyli bez konieczności dodatkowej konfiguracji (wystarczy, że twój projekt posiada zależności do JUnit 5). W celu uruchomienia wszystkich testów z danej klasy testowej wybierz opcję Run <Nazwa klasy testowej>:

Rezultaty wykonania znajdziesz w oknie narzędziowym Run:

Tip: W IntelliJ istnieje kilka możliwości tworzenia konfiguracji i uruchamiania testów. Osobiście polecam korzystanie ze skrótów klawiszowych (Ctrl+Shift+F10). Więcej informacji znajdziesz w oficjalnej dokumentacji: https://www.jetbrains.com/help/idea/creating-test-methods.html.

Wybrane testy możesz również uruchomić z wiersza poleceń za pomocą Gradle.

Uruchom cmder i w katalogu domowym projektu wprowadź następujące polecenie:

gradlew clean test --tests *TodoMvcTests

Polecenie uruchomi testy z klasy pl.qalabs.blog.junit5.selenium.todomvc.TodoMvcTests.

Tip: Możesz korzystać z wiersza poleceń bezpośrednio w IntelliJ. Terminal możesz uruchomić używając skrótu Alt+F12. Integracja terminala IntelliJ z Git Bash została opisana tutaj: Narzędzia - Java, Gradle i IntelliJ

Inicjalizacja sterownika

Klasa testowa, oprócz dwóch metod testowych, posiada dwie metody dodatkowe: beforeEach i afterEach. W standardowym cyklu życia testu w JUnit 5, każda metoda oznaczona adnotacją @BeforeEach i @AfterEach wykonana zostanie odpowiednio przed i po każdej metodzie testowej w danej klasie testowej. W naszym przykładzie, metody te wykorzystamy do zarządzania środowiskiem dla testów. Przed każdym testem zaincjalizujemy sterownik Selenium WebDriver, który uruchomi wybraną przeglądarkę. Natomiast po każdym teście zamkniemy przeglądarkę, bez względu na wynik testu.

Zmodyfikuj CreateTodoTest w następujący sposób:

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
package pl.qalabs.blog.junit5.selenium.todomvc;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import java.util.concurrent.TimeUnit;

@DisplayName("Managing Todos")
class TodoMvcTests {

private WebDriver driver;

@BeforeEach
void beforeEach() {
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(500, TimeUnit.MILLISECONDS);
driver.get("http://todomvc.com/examples/vanillajs");
}

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

Metoda beforeEach inicjalizuje ChromeDriver z domyślnymi ustawieniami, zmienia domyślny timeout dla operacji wyszukiwania elementów na 500 milisekund oraz otwiera testowaną aplikację.
Uruchom testy i obserwuj, czy przeglądarka Chrome uruchomiła się i zamknęła dwukrotnie (dla każdego testu).

Tip: Nie jesteś pewien, czy Twoja konfiguracja jest prawidłowa? Zajrzyj tutaj: Narzędzia - Selenium i przeglądarki internetowe

Sterownik chromedriver musi być dostępny w PATH. W sytuacji, gdy sterownik nie znajduje się w PATH lub używasz kilku wersji sterownika, możesz użyć zmiennej systemowej webdriver.chrome.driver (dla innych sterowników nazwa zmiennej będzie inna) w celu wskazania dokładnej lokalizacji pliku sterownika:

1
2
3
4
5
6
7
@BeforeEach
void beforeEach() {
System.setProperty("webdriver.chrome.driver", "PATH TO YOUR CHROME DRIVER EXECUTABLE");
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(500, TimeUnit.MILLISECONDS);
driver.get("http://todomvc.com/examples/vanillajs");
}

Plik uruchomieniowy przeglądarki wyszukiwany jest w domyślnej lokalizacji dla systemu operacyjnego, w którym test jest uruchamiany. W celu wskazania lokalizacji pliku uruchamiającego przeglądarkę należy odpowiednio zainicjalizować sterownik. Dla ChromeDriver, jedną z opcji to użycie obiektu ChromeOptions, który posiada między innymi właściwość zawierającą lokalizację pliku wykonywalnego przeglądarki Chrome:

1
2
3
4
5
6
7
8
9
import org.openqa.selenium.chrome.ChromeOptions;

@BeforeEach
void beforeEach() {
System.setProperty("webdriver.chrome.driver", "PATH TO YOUR CHROME DRIVER EXECUTABLE");
driver = new ChromeDriver(new ChromeOptions().setBinary("PATH TO YOUR CHROME EXECUTABLE"));
driver.manage().timeouts().implicitlyWait(500, TimeUnit.MILLISECONDS);
driver.get("http://todomvc.com/examples/vanillajs");
}

Tip: Zajrzyj do klas HelloSeleniumChromeTest oraz HelloSeleniumFirefoxTest, w których znajdziesz przykłady konfiguracji sterowników dla przeglądarki Chrome i Firefox.

Test - Tworzenie zadania

Automatyzacja scenariusza tworzenia nowego zadania polegać będzie na wykonaniu następujących akcji użytkownika:

  • Utworzenie nowego zadania po wpisaniu tytułu i wciśnięciu <Enter>
  • Weryfikacji wartości <number> dla elementu <number> items left
  • Weryfikacji, że zadanie pojawiło się na liście

Zmodyfikuj metodę testową createsTodo():

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
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@DisplayName("Managing Todos")
class TodoMvcTests {

@Test
@DisplayName("Creates Todo with given title")
void createsTodo() {
// create new item
WebElement todoInput = driver.findElement(By.className("new-todo"));
todoInput.sendKeys("My Todo 1");
todoInput.sendKeys(Keys.ENTER);

// verify count
WebElement todoCount = driver.findElement(By.cssSelector(".todo-count > strong"));
assertEquals(1, Integer.parseInt(todoCount.getText()));

// verify item in the list
List<WebElement> todoList = driver.findElements(By.cssSelector(".todo-list li"));
assertTrue(todoList.stream().anyMatch(el -> "My Todo 1".equals(el.getText())));
}
}

W celu utworzenia nowego zadania znajdujemy pole tekstowe o klasie .new-todo i wpisujemy do niego tytuł a następnie naciskamy <Enter> (przez wpisanie znaku specjalnego). Po utworzeniu rekordu, wartość w elemencie o selektorze .todo-count > strong zmieniła się. Pobieramy tę wartość i sprawdzamy, czy jest równa 0, używając wbudowanej asercji assertEquals z JUnit 5. W kolejnym kroku, pobieramy wszystkie zadania używając selektora .todo-list li i sprawdzamy, czy w liście znajduje się wcześniej dodane zadanie.

Test - Edycja zadania

Automatyzacja scenariusza edycji istniejącego zadania polegać będzie na wykonaniu następujących akcji użytkownika:

  • Utworzenie nowego zadania
  • Wyszukanie zadania do edycji
  • Przejście do edycja zadania
  • Edycja zadania i zapisanie zmian
  • Weryfikacji, że zmienione zadanie pojawiło się na liście

Zmodyfikuj metodę testową editsTodo():

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
import org.openqa.selenium.interactions.Actions;

@DisplayName("Managing Todos")
class TodoMvcTests {

@Test
@DisplayName("Edits inline double-clicked Todo")
void editsTodo() {
// create new item
WebElement todoInput = driver.findElement(By.className("new-todo"));
todoInput.sendKeys("My Todo 2");
todoInput.sendKeys(Keys.ENTER);

// get item by name
WebElement todoLi = driver.findElements(By.cssSelector(".todo-list li"))
.stream()
.filter(el -> "My Todo 2".equals(el.getText()))
.findFirst()
.orElseThrow(() -> new AssertionError("Test data missing!"));
// edit item
new Actions(driver).doubleClick(todoLi).perform();

WebElement todoEditInput = todoLi.findElement(By.cssSelector("input.edit"));
todoEditInput.sendKeys(Keys.chord(Keys.CONTROL, "a", Keys.DELETE), "My Changed Todo 2");
todoEditInput.sendKeys(Keys.ENTER);

// verify changed item in the list
List<WebElement> todoList = driver.findElements(By.cssSelector(".todo-list li"));
assertTrue(todoList.stream().anyMatch(el -> "My Changed Todo 2".equals(el.getText())));
}
}

Do edycji potrzebne jest istniejące zadanie, dlatego w teście zostanie ono utworzone. Nowe zadanie należy odszukać na liście zadań, a następnie dwukrotnie je kliknąć. W celu obsługi operacji niskopoziomowych, takich jak podwójne kliknięcie czy ruch myszą, wykorzystany został obiekt klasy org.openqa.selenium.interactions.Actions.
Podwójne kliknięcie elementu na liście powoduje pojawienie się w DOM pola tekstowego. To pole należy odnaleźć, a następnie wprowadzić do niego nową wartość. Zmiany w rekordzie zapisane będą po naciśnięciu klawisza <Enter>. Natomiast weryfikacja, czy zmieniony element znajduje się na liście, jest niemal identyczna jak w poprzednim teście.

Tip: Zwróć uwagę na użycie w testach wyrażeń Lambda. Wyrażenia Lambda pozwalają między innymi na tworzenie bardziej zwięzłego kodu. Więcej informacji znajdziesz tutaj: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

Test - Usuwanie zadania

Automatyzacja scenariusza usuwania istniejącego zadania polegać będzie na wykonaniu następujących akcji użytkownika:

  • Utworzenie nowego zadania
  • Wyszukanie zadania do edycji
  • Ruch kursora myszy nad znalezione zadanie
  • Usunięcia zadania
  • Weryfikacji, że usunięte zadanie nie znajduje się na liście

Zmodyfikuj metodę testową removesTodo():

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
@Test
@DisplayName("Removes selected Todo")
void removesTodo() {
// create new item
WebElement todoInput = driver.findElement(By.className("new-todo"));
todoInput.sendKeys("My Todo 3");
todoInput.sendKeys(Keys.ENTER);

// get item by name
WebElement todoLi = driver.findElements(By.cssSelector(".todo-list li"))
.stream()
.filter(el -> "My Todo 3".equals(el.getText()))
.findFirst()
.orElseThrow(() -> new AssertionError("Test data missing!"));

// move mouse to item
new Actions(driver).moveToElement(todoLi).perform();

// remove item
WebElement todoDestroyButton = todoLi.findElement(By.cssSelector("button.destroy"));
todoDestroyButton.click();

// verify no item in the lists
List<WebElement> todoList = driver.findElements(By.cssSelector(".todo-list li"));
assertTrue(todoList.stream().noneMatch(el -> "My Todo 3".equals(el.getText())));
}

Ruch myszą symulowany jest ponownie z wykorzystaniem obiektu klasy org.openqa.selenium.interactions.Actions. Usunięcie elementu następuje po kliknięciu przycisku z klasą .destroy . Podobnie jak wcześniej, weryfikacja polega na sprawdzeniu, czy element nie znajduje się na liście.

Test - Oznaczenie zadania jako zakończonego

Automatyzacja scenariusza oznaczania zadania jako zakończonego polegać będzie na wykonaniu następujących akcji użytkownika:

  • Utworzenie dwóch nowych zadań i oznacznie ich jako zakończonych
  • Utworzenie kolejnego zadania
  • Usunięcie zakończonych zadań
  • Weryfikacji liczby elementów (również z użyciem filtrów)

Zmodyfikuj metodę testową togglesTodoCompleted:

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
@Test
@DisplayName("Toggles selected Todo as completed")
void togglesTodoCompleted() {
// create new items
WebElement todoInput = driver.findElement(By.className("new-todo"));
todoInput.sendKeys("My Todo 4.1");
todoInput.sendKeys(Keys.ENTER);

todoInput.sendKeys("My Todo 4.2");
todoInput.sendKeys(Keys.ENTER);

// get item by name
WebElement todoLi = driver.findElements(By.cssSelector(".todo-list li"))
.stream()
.filter(el -> "My Todo 4.1".equals(el.getText()))
.findFirst()
.orElseThrow(() -> new AssertionError("Test data missing!"));

// toggle item completed
WebElement todoToggleCheckbox = todoLi.findElement(By.cssSelector("input.toggle"));
todoToggleCheckbox.click();

// verify count
WebElement todoCount = driver.findElement(By.cssSelector(".todo-count > strong"));
assertEquals(1, Integer.parseInt(todoCount.getText()));

// show completed items
WebElement completedLink = driver.findElement(By.cssSelector("a[href='#/completed']"));
completedLink.click();

// verify completed items count
List<WebElement> completedTodos = driver.findElements(By.cssSelector(".todo-list li"));
assertEquals(1, completedTodos.size());

// show active items
WebElement activeLink = driver.findElement(By.cssSelector("a[href='#/active']"));
activeLink.click();

// verify active items count
List<WebElement> activeTodos = driver.findElements(By.cssSelector(".todo-list li"));
assertEquals(1, activeTodos.size());
}

Test - Oznaczenie wielu zadań jako zakończonych

Scenariusz oznaczenia wielu zadań jako zakończonych nie różni się wiele od poprzedniego, zatem nie wymaga dodatkowego wyjaśnienia.

Zmodyfikuj metodę testową togglesAllTodosCompleted:

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
@Test
@DisplayName("Toggles all Todos as completed")
void togglesAllTodosCompleted() {
// create new items
WebElement todoInput = driver.findElement(By.className("new-todo"));
todoInput.sendKeys("My Todo 5.1");
todoInput.sendKeys(Keys.ENTER);

todoInput.sendKeys("My Todo 5.2");
todoInput.sendKeys(Keys.ENTER);

// toggle all items
WebElement toggleAllCheckbox = driver.findElement(By.className("toggle-all"));
toggleAllCheckbox.click();

// verify count
WebElement todoCount = driver.findElement(By.cssSelector(".todo-count > strong"));
assertEquals(0, Integer.parseInt(todoCount.getText()));

// show completed items
WebElement completedLink = driver.findElement(By.cssSelector("a[href='#/completed']"));
completedLink.click();

// verify completed items count
List<WebElement> completedTodos = driver.findElements(By.cssSelector(".todo-list li"));
assertEquals(2, completedTodos.size());

// show active items
WebElement activeLink = driver.findElement(By.cssSelector("a[href='#/active']"));
activeLink.click();

// verify active items count
List<WebElement> activeTodos = driver.findElements(By.cssSelector(".todo-list li"));
assertEquals(0, activeTodos.size());
}

Test - Usunięcie zakończonych zadań

Automatyzacja scenariusza usuwania zakończonych zadań polegać będzie na wykonaniu następujących akcji użytkownika:

  • Utworzenie dwóch nowych zadań i oznacznie ich jako zakończonych
  • Utworzenie kolejnego zadania
  • Usunięcie zakończonych zadań
  • Weryfikacji liczby elementów (również z użyciem filtrów)

Zmodyfikuj metodę testową clearsCompletedTodos:

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
@Test
@DisplayName("Clears all completed Todos")
void clearsCompletedTodos() {
// create new items
WebElement todoInput = driver.findElement(By.className("new-todo"));
todoInput.sendKeys("My Todo 6.1");
todoInput.sendKeys(Keys.ENTER);

todoInput.sendKeys("My Todo 6.2");
todoInput.sendKeys(Keys.ENTER);

// toggle all items
WebElement toggleAllCheckbox = driver.findElement(By.className("toggle-all"));
toggleAllCheckbox.click();

// create new item
todoInput.sendKeys("My Todo 6.3");
todoInput.sendKeys(Keys.ENTER);

// clear completed items
WebElement clearCompletedButton = driver.findElement(By.className("clear-completed"));
clearCompletedButton.click();

// verify count
WebElement todoCount = driver.findElement(By.cssSelector(".todo-count > strong"));
assertEquals(1, Integer.parseInt(todoCount.getText()));

// show completed items
WebElement completedLink = driver.findElement(By.cssSelector("a[href='#/completed']"));
completedLink.click();

// verify completed items count
List<WebElement> completedTodos = driver.findElements(By.cssSelector(".todo-list li"));
assertEquals(0, completedTodos.size());

// show active items
WebElement activeLink = driver.findElement(By.cssSelector("a[href='#/active']"));
activeLink.click();

// verify active items count
List<WebElement> activeTodos = driver.findElements(By.cssSelector(".todo-list li"));
assertEquals(1, activeTodos.size());
}

Uruchamianie testów w Firefox

Aktualnie testy uruchamiane są w przeglądarce Chrome. Ale czy zadziałają w Firefox? Warto to sprawdzić. Aby to zrobić, wystarczy w metodzie beforeEach() podmienić implementację klasy WebDriver z ChromeDriver na FirefoxDriver:

1
2
3
4
5
6
7
8
import org.openqa.selenium.firefox.FirefoxDriver;

@BeforeEach
void beforeEach() {
driver = new FirefoxDriver();
driver.manage().timeouts().implicitlyWait(500, TimeUnit.MILLISECONDS);
driver.get("http://todomvc.com/examples/vanillajs");
}

Po uruchomieniu testów w przeglądarce Firefox okazuje się, że nie wszystkie zadziałały prawidłowo. Test editsTodo kończy się wyjątkiem w linii todoEditInput.sendKeys("My Changed Todo 2");:

org.openqa.selenium.StaleElementReferenceException: The element reference of <input class="edit"> /
is stale; either the element is no longer attached to the DOM, it is not in the current frame context, /
or the document has been refreshed

Sam wyjątek oznacza, że element <input class="edit">, do którego test próbuje wpisać tekst, nie istnieje już w DOM.

Najbardziej prawdopodobną przyczyną tego błędu jest inny sposób obsługi metody todoEditInput.clear() w przeglądarce Firefox (jest ona wywoływana bezpośrednio przed problematyczną instrukcją). Obserwując zachowanie aplikacji w trakcie testu widać, że w Firefox metoda clear() powoduje nie tylko usunięcie treści, ale także zapisanie pustej wartości pola, bo po wywołaniu metody todoEditInput.clear() edytowane zadanie zostaje usunięte.
Ten problem można obejść zmieniając sposób wyczyszczenia zawartości todoEditInput - zamiast wywoływać metodę clear() wykonać kod JavaScript, który zmieni wartość właściwości value dla tego pola. Nie jest to idealne rozwiązanie, ze względu na fakt, iż rzeczywisty użytkownik nie wykonuje w aplikacji tego typu działań, trzeba jednak pogodzić się z tym, że w testach automatycznych nie zawsze mamy możliwość pełnej symulacji pracy użytkownika.

W celu wykonywania kodu JavaScript w naszym teście musimy dokonać następujących zmian:

  • Zmienić typ pola driver z org.openqa.selenium.WebDriver na org.openqa.selenium.remote.RemoteWebDriver (interfejs org.openqa.selenium.WebDriver nie posiada deklaracji potrzebnej nam metody executeScript)
  • Zmodyfikować metodę editsTodo, w miejsce instrukcji todoEditInput.clear() wprowadzić nową instrukcję, która wykona kod JavaScript: driver.executeScript("arguments[0].value = ''", todoEditInput); (arguments[0] to todoEditInput, którego właściwość value zostanie zmieniona).

Po dokonaniu powyższych zmian, testy w obu przeglądarkach powinny wykonywać się poprawnie.

Tip: Różnice wynikające z implementacji tych samych metod w różnych przeglądarkach mogą pojawiać się częściej. Jeżeli planujesz uruchamiać testy na kilku przeglądarkach, upewnij się, że przed ich ostateczną akceptacją i umieszczeniem w repozytorium uruchomiłeś je we wszystkich przeglądarkach, których używasz w testach.
Zmiana implementacji sterownika w kodzie nie jest optymalna i na pewno nie jest zalecana. Dlatego w kolejnych artykułach zademonstruję, w jaki sposób można to zoptymalizować.

Dalsze kroki

Ten szczegół to nie jedyny element, który w przedstawionym kodzie można poprawić lub zoptymalizować. Nietrudno zauważyć inne problemy, które mogłyby wpłynąć na łatwość utrzymania i rozszerzania testów w przyszłości. Przykładem mogą być choćby długie i nieczytelne metody testowe oraz liczne powtórzenia kodu, które łamią jedną z fundamentalnych zasad tworzenia oprogramowania, czyli DRY - Don’t Repeat Yourself. Testy będą rozwijane i poprawiane w kolejnych artykułach, w których pokażę bardziej zaawansowane funkcjonalności frameworka JUnit 5 oraz dobre praktyki tworzenia tego typu kodu.

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 branchu 1-getting-started

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.