The service provided by Consileon was professional and comprehensive with a very good understanding of our needs and constrains.

Wolfgang Hafenmayer, Managing partner, LGT Venture Philanthropy

Technical quality of staff offered, capability of performing various project roles, as well as motivation and dedication to the project (... [...]

dr Walter Benzing, Head of development B2O, net mobile AG

Technical quality of staff offered, capability of performing roles of developers, lead developers, trainers, team leaders, architects as wel [...]

Karl Lohmann, Itellium Systems & Services GmbH

Firma Consileon Polska jest niezawodnym i godnym zaufania partnerem w biznesie, realizującym usługi z należytą starannością (...)

Waldemar Ściesiek, Dyrektor zarządzający IT, Polski Bank

The team was always highly motivated and professional in every aspect to perform on critical needs of our startup environment.

Denis Benic, Founder of Ink Labs

Selenide – testy UI jeszcze prostsze

Category: Testing Tags: , ,

Pisanie szybkich, stabilnych i efektywnych testów automatycznych jest prawdziwym wyzwaniem. Do tego celu potrzebujesz właściwych narzędzi.

Selenide jest biblioteką do tworzenia łatwych do czytania i utrzymywania, stabilnych automatycznych testów dla aplikacji webowych w Javie, Scali, Groovym, czy Clojure, czyli każdym języku bazującym na JVM. Wspiera najpopularniejsze platformy: Windows, Linux oraz OS X oraz przeglądarki. Narzędzie to opakowuje Selenium Web Driver w wersji 2.0, definiując spójne i przejrzyste API oraz assercje w języku naturalnym.

Wymagania

  • Java SE JDK 1.7+
  • Eclipse 4.2+ lub inne IDE do Javy
  • Maven, Ivy lub Gradle
  • JUnit, TestNG lub inny framework do testów (Cucumber, ScalaTest, JBehave)
  • Chrome lub inna przeglądarka (Firefox, IE, Opera, PhantomJS, HtmlUnit, Marionette)

Rozpoczęcie pracy

W Eclipse lub bezpośrednio w Mavenie tworzymy nowy projekt, a następnie w pom.xml dodajemy zależność do Selenide:

<dependency>
    <groupId>com.codeborne</groupId>
    <artifactId>selenide</artifactId>
    <version>4.4.1</version>
    <scope>test</scope>
</dependency>

oraz JUnit:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

Teraz możemy utworzyć pierwszą klasę z testem, który:

  1. otworzy przeglądarkę i wejdzie na stronę google.pl;
  2. znajdzie odpowiedni element i wstawi do niego szukany tekst Selenide, zatwierdzając go naciśnięciem enter
  3. a w rezultacie skontroluje liczbę znalezionych pozycji na pierwszej stronie oraz zawartość pierwszego linku
@Test
public void checkGoogleSearch() {
    open("https://www.google.pl");
    $(By.name("q")).setValue("Selenide").pressEnter();
    $$("#ires div.g").shouldHave(size(8));
    $("#ires .g").shouldBe(visible).shouldHave(
        text("Selenide: concise UI tests in Java"),
        text("selenide.org")
    );
}

Aby ułatwić sobie pracę, najlepiej jeszcze zaimportować metody potrzebne do testów:

import static com.codeborne.selenide.CollectionCondition.*;
import static com.codeborne.selenide.Condition.*;
import static com.codeborne.selenide.Selectors.*;
import static com.codeborne.selenide.Selenide.*;
import org.openqa.selenium.By

Konfiguracja i dostosowywanie

Selenide posiada bardzo rozsądne domyślne ustawienia, które powinny być odpowiednie dla większości projektów. Zmiany konfiguracji możemy dokonać na kilka sposobów:

  • poprzez właściwości systemu (system property) – jako parametr uruchomieniowy (argument maszyny wirtualnej):
-Dselenide.timeout=6000 -Dselenide.baseUrl=http://consileon.pl
  • programowo poprzez właściwości systemu:
System.setProperty("selenide.timeout ", "6000");
System.setProperty("selenide.baseUrl", "http://consileon.pl");
  • programowo nadpisując zmienne w klasie Configuration z pakietu selenide, która zawiera różne opcje konfiguracji dla testów:
Configuration.timeout = 6000;
Configuration.baseUrl = "http://consileon.pl";

Dostosowywanie

Autorzy projektu chwalą się, że zaprojektowali Selenide w taki sposób, że prawie każdy najmniejszy kawałek logiki w razie potrzeby może zostać łatwo dostosowany – można uzupełnić lub zastąpić dowolną metodę. Na przykład można zmienić sposób, w jaki tworzone są zrzuty ekranów nadpisując istniejącą logikę, dostarczając własną wersję ScreenShotLaboratory:

public class SelenideConfiguration {

	static {
		Screenshots.screenshots = new ScreenShotLaboratory() {

			@Override
			protected void copyFile(InputStream in, File targetFile) throws IOException {
				super.copyFile(in, targetFile);
				// Additional logic here
			}

		};
	}
}

Browser capabilities

Jeżeli potrzebujemy dodać dodatkowe możliwości do przeglądarki, to trzeba odpowiednio skonfigurować WebDriver. Można to zrobić modyfikując metodę tworzącą driver. Poniżej przykład aktywowania JavaScript i robienia zdjęć oraz dezaktywowania walidacji SSL dla PhantomJS:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;
import org.openqa.selenium.remote.DesiredCapabilities;

import com.codeborne.selenide.WebDriverProvider;

public class CustomPhantomJSDriver implements WebDriverProvider {

	@Override
	public WebDriver createDriver(DesiredCapabilities desiredCapabilities) {

		desiredCapabilities.setJavascriptEnabled(true);
		desiredCapabilities.setCapability("takesScreenshot", true);
		desiredCapabilities.setCapability("phantomjs.cli.args", new String[] {"--ignore-ssl-errors=true"});

		return new PhantomJSDriver(desiredCapabilities);
	}

}

Emulacja urządzeń mobilnych

Istnieje również możliwość uruchamiania testów, emulując różnego typu urządzenia mobilne. Tym samym możemy upewnić się, że nasza aplikacja działa prawidłowo na przykład na: Apple iPad, Apple iPhone 6 Plus, Samsung Galaxy S5, czy Google Nexus X5:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.DesiredCapabilities;

import com.codeborne.selenide.WebDriverProvider;

public class ChromeMobile implements WebDriverProvider {

	@Override
	public WebDriver createDriver(DesiredCapabilities desiredCapabilities) {
		Map<String, String> mobileEmulation = new HashMap<>();
		mobileEmulation.put("deviceName", "Apple iPad");

		Map<String, Object> chromeOptions = new HashMap<>();
		chromeOptions.put("mobileEmulation", mobileEmulation);

		DesiredCapabilities capabilities = DesiredCapabilities.chrome();
		capabilities.setCapability(ChromeOptions.CAPABILITY, chromeOptions);

		return new ChromeDriver(capabilities);
	}

}

Więcej informacji można znaleźć na stronach Google.

Selenide kontra Selenium

W porównaniu do Selenium, Selenide wypada bardzo obiecująco. Jest prostszy i potężniejszy. API Selenide jest napisane na wyższym poziomie abstrakcji niż Selenium, ale to dlatego, że jest dedykowane do testów UI i akceptacyjnych. Poniżej najciekawsze zestawienie różnic:

Otwieranie i zamykanie przeglądarki

W Selenide nie musimy bezpośrednio operować na poziomie WebDrivera. Będzie on automatycznie utworzony/usunięty podczas startu/zakończenia testu. Przeglądarkę uruchamiamy metodą open("https://www.google.pl");, a o jej zamknięcie nie musimy się już martwić.

W Selenium WebDriver trzeba było posprzątać. Na przykład: w metodzie oznaczonej adnotacją @BeforeClass dokonywaliśmy inicjalizacji drivera: driver = new FirefoxDriver();, w metodzie z adnotacją @Test wchodziliśmy na testowaną stronę: driver.get("https://www.google.pl");, a na końcu w metodzie oznaczonej adnotacją @AfterClass zamykaliśmy przeglądarkę: driver.close();. Jak widać, musimy zadbać o wszystkie szczegóły.

Przejrzyste asercje

Budowanie asercji jest proste i intuicyjne, czyta się je jak tekst napisany w języku naturalnym, a dopasowywać można prawie wszystko:

$("#footer").shouldHave(text("Copyright"));
$("#mainContainer").shouldHave(cssClass("error"));
$("#mainContainer").should(matchText("form"));
$("#mainContainer").shouldNot(exist);

w porównaniu do tego, co możemy napisać w Selenium WebDriver:

assertEquals("Copyright", driver.findElement(By.id("footer")).getText());
assertTrue(driver.findElement(By.id("mainContainer")).getAttribute("class").indexOf("error") > -1);
assertTrue(Pattern.compile(".*form.*", DOTALL).matcher(driver.findElement(By.id("mainContainer")).getText()).matches());
assertTrue(driver.findElements(By.id("mainContainer")).isEmpty());

Lokalizowanie

Lokalizowanie pierwszego elementu po selektorze CSS w Selenide:

$("selector");

zastępuje:

driver.findElement(By.cssSelector("selector"));

Z kolei lokalizowanie wszystkich elementów po selektorze CSS w Selenide:

$$("selector");

zastępuje:

driver.findElements(By.cssSelector("selector"));

Szukanie elementu

Aby znaleźć element po id, posłużymy się metodą:

WebElement footer = $("#footer");

lub bardziej tradycyjnie:

WebElement footer = $(By.id("footer"));

z kolei w Selenium jest to trochę mniej przejrzyste ze względu na odwoływanie się do drivera:

WebElement footer = driver.findElement(By.id("footer"));

W Selenide szukanie po tekście jest niezmiernie łatwe:

WebElement form = $(byText("Login form"));

z kolei w Selenium WebDriver musimy sięgnąć po xpath:

WebElement form = driver.findElement(xpath("//text() = ‘Login form’"));

Szukanie element wewnątrz innego (rodzica) również jest bardzo proste:

WebElement button = $("#mainContainer").find(".btn");

w stosunku do:

WebElement button = driver.findElement(By.id("mainContainer")).findElement(By.className("btn"));

Podobnie łatwo przedstawia się szukanie n-tego element:

WebElement item = $("li", 6);

w przeciwieństwie do:

WebElement item = driver.findElements(By.tagName("li")).get(6);

Alerty

Aby zatwierdzić dialog z alertem poprzez kliknięcie „OK” wystarczy:

confirm("Are you accept loan conditions?");

w Selenium WebDriver można użyć:

driver.switchTo().alert().accept();

jednak nie możemy wtedy skontrolować wiadomości alertu, dlatego poprawniej będzie:

try {
	Alert alert = checkAlertMessage("Are you accept loan conditions?");
	alert.accept();
	Thread.sleep(250);
} catch (UnsupportedOperationException | InterruptedException e) {
	return;
}

Informacje do debugowania

SelenideElement ma przeciążoną metodę toString(), która zapewnia wyświetlenie czytelnych danych, które można wykorzystać w debugowaniu lub logowaniu:

System.out.println($(By.name("q")));

wyświetli:

<input aria-autocomplete="both" aria-haspopup="false" aria-label="Szukaj" autocomplete="off" class="gsfi lst-d-f" dir="ltr" id="lst-ib" maxlength="2048" name="q" role="combobox" spellcheck="false" title="Szukaj" type="text"></input>

w Selenium WebDriver musielibyśmy wyciągać każdą własność oddzielnie:

WebElement element = driver.findElement(By.name("q"));
System.out.println("text: " + element.getText());
System.out.println("id: " + element.getAttribute("id"));
System.out.println("name: " + element.getAttribute("name"));
System.out.println("class: " + element.getAttribute("class"));
System.out.println("title: " + element.getAttribute("title"));
System.out.println("visible: " + element.isDisplayed());

Zrzuty ekranu

Aby wykonać zrzut ekrany wystarczy użyć metody Screenshots.takeScreenShot("fileName"), a kiedy dodatkowo zaprzęgniemy do tego JUnit, to możemy utworzyć regułę, dzięki której screenshot będzie robiony automatycznie, gdy test nie przejdzie:

@Rule
public ScreenShooter makeScreenshotOnFailure = ScreenShooter.failedTests();

w przypadku Selenium WebDriver jest to bardziej skomplikowane:

if (driver instanceof TakesScreenshot) {
	File scrFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
	File targetFile = new File("c:\temp\" + fileName + ".png");
	FileUtils.copyFile(scrFile, targetFile);
}

Domyślnie zrzuty zapisywane są w folderze build/reports/tests, ale można to zmienić ustawiając parametr:

-Dselenide.reports=test-result/reports

lub bezpośrednio w kodzie:

Configuration.reportsFolder = "test-result/reports";

Zaznaczenie radio buttona

W celu wybrania radio buttona wystarczy:

$(By.name("gender")).selectRadio("other");

po raz kolejny, używając Selenium WebDriver musimy się trochę rozpisać, na przykład:

for (WebElement radio : driver.findElements(By.name("gender"))) {
	if ("other".equals(radio.getAttribute("value"))) {
		radio.click();
		return;
	}
}
throw new NoSuchElementException("there is no value 'other' for 'gender' radio field");

Przeładowanie strony

Wystarczy wywołać metodę: refresh(), natomiast w Selenium WebDriver wygląda to miej korzystnie:

driver.navigate().to(driver.getCurrentUrl());

Pobranie urla i tytułu strony oraz jej kodu źródłowego

Na tym polu Selenide również idzie nam z pomocą, dostarczając kolejne trzy metody:

url();
title();
source();

i nie musimy się więc odwoływać do WebDrivera:

driver.getCurrentUrl();
driver.getTitle();
driver.getPageSource();

Ajax i timeouty

Selenide oferuje nam inteligentne oczekiwanie na pojawienie się elementu lub na to aż się dany element zmieni. Domyślnie czas ten wynosi cztery sekundy, ale można go zmienić (parametr: selenide.timeout). Poniższa komenda oczekuje, aż szukany element stanie się widoczny i będzie miał oczekiwaną wartość:

$(" textarea").shouldHave(value("Selenide"));

więc nie musimy się odwoływać do WebDrivera:

By by = By.tagName("textarea");
new WebDriverWait(driver, 4).until(ExpectedConditions.presenceOfElementLocated(by));
new WebDriverWait(driver, 4).until(ExpectedConditions.visibilityOf(driver.findElement(by)));
assertEquals("Selenide", driver.findElement(by).getAttribute("value"));

W rzadkich przypadkach możemy chcieć zmienić domyślny czas oczekiwania dla wybranego element:

$("# textarea").waitUntil(hasText("Selenide"), 8000);

Możliwości

Selenide dostarcza wiele funkcjonalności. Poniżej przedstawione zostały niektóre z nich, które zostały pominięte wcześniej. API Selenide składa się z kilku klas, dlatego najlepiej od razu uruchomić swoje IDE i zacząć pracę wpisując symbol dolara $, kropkę . i wybierając spośród dostępnych opcji sugerowanych przez IDE, tym samym natychmiast możemy koncentrować się na logice biznesowej.

Praktycznie wszystko, czego potrzebujemy do pracy z Selenide znajduje się w kilku klasach z pakietu selenide:

  • Selenide – główna klasa biblioteki, zawierająca podstawowe metody: open, $ i $$,
  • SelenideElement – opisuje element znaleziony na stronie; obiekt tej klasy można dostać np. przez polecenie $; mamy tu np.: find, should, waitUntil, click, hover, text, isDisplayed, exists
  • ElementsCollection – opisuje zbiór elementów na stronie; zwykle obiekt tej klasy można uzyskać za pomocą metody $$; mamy tu np.: shouldBe, shouldHave, find, filter
  • Condition – warunki używane są w konstrukcjach typu: should / shouldNot / waitUntil / waitWhile; mamy tu np.: visible, hidden, matchText
  • CollectionCondition – warunki są używane w konstrukcjach typu: shouldBe / shouldHave; mamy tu np.: sizeLessThan, texts
  • Selectors – zawierająca selektory By do zlokalizowania elementów poprzez tekst lub atrybuty; mamy tu np.: byText, withText, byName
  • WebDriverRunner – definiuje niektóre metody do zarządzania przeglądarką

Więcej informacji można znaleźć w dokumentacji. Ciekawe zestawienie przydatnych w pracy metod można znaleźć na stronie Selenide cheat sheet.

Jeżeli brakuje czegoś w Selenide, można oczekiwać, że znajduje się to w Selenium. Na przykład, aby przełączyć się między tabami/oknami przeglądarki, należy skorzystać z metod: webDriver.getWindowHandles() oraz webDriver.getWindowHandle().

Kolekcje

Selenide pozwala na pracę z kolekcjami, umożliwiając w ten sposób sprawdzanie (testowanie) wiele elementów w jednej linijce kodu:

$$(".error").shouldHave(size(3));
$$("#actors tbody tr")
  .shouldHave(size(4))
  .shouldHave(
    texts(
      "John Malkovich",
      "Bruce Willis"
  );

Filtrowanie

Na szczególną uwagę zasługuje możliwość filtrowania elementów kolekcji ElementsCollection dzięki metodzie filterBy(Condition condition) / filter(Condition condition), która umożliwia wyłuskanie interesujących nas elementów z kolekcji:

$$("#actors tbody tr").filterBy(visible).filter(text("John"));

Wysyłanie i pobieranie plików

Aby wysłać pojedynczy plik użyjemy:

$("#cv").uploadFile(new File("cv.docx"));

a w przypadku wielu plików:

$("#cv").uploadFile(
  new File("cv1.docx"),
  new File("cv3.docx")
);

Pobranie pliku jest równie proste:

File pdf = $(".btn#cv").download();

Page Object

Selenide wspiera page object i nie ma potrzeby robić niczego specjalnego, aby to zastosować; nie potrzeba adnotacji, konstruktorów, PageFactory czy instancji WebDriver. Wystarczy utworzyć dowolną klasę, która ukryje całą logikę potrzebną do pracy z elementami w ramach danej strony, na przykład:

public class GooglePage {

	public void searchFor(String query) {
		$(By.name("q")).setValue(query).pressEnter();
	}

	public ElementsCollection getResults() {
		return $$("#ires div.g");
	}

	public SelenideElement getResult(int index) {
		return $("#ires div.g", index);
	}

}

którą następnie w prosty sposób można użyć:

public class GooglePageTest {

	@Test
	public void checkGoogleSearch() {
		GooglePage page = open("https://www.google.pl", GooglePage.class);
		page.searchFor("Selenide");
		Assert.assertEquals(8, page.getResults().size());
		Assert.assertTrue(page.getResult(0).getText().contains("selenide.org"));
	}

}

Problemy

Niestety z używaniem Selenide wiąże się kilka problemów. Jednak na obronę można dodać, że dotyczą one raczej ukrytego pod spodem Selenium.

Firefox

Domyślnie Selenide uruchamia testy w przeglądarce Firefox. Nie było by w tym nic złego, gdyby nie to, że Selenium WebDriver nie nadąża z ciągłymi aktualizacjami przeglądarki. Jeżeli driver w Selenium jest za stary, to będzie niekompatybilny z najnowszą wersją Firefoxa, i próba odpalenia testu wygeneruje błąd:

INFO: Firefox 48+ is currently not supported by Selenium Firefox driver. Use browser=marionette with geckodriver, when using it.

Zaradzić temu można próbując zablokować automatyczne aktualizacje przeglądarki, aby pewnego dnia nie okazało się, że automatyczna aktualizacja sprawiła, iż wszystkie nasze testy zaczęły padać, a zazwyczaj to się dzieje w najmniej odpowiednim momencie. W celu zmiany konfiguracji Firefoxa, trzeba wejść do Opcje | Ustawienia | Aktualizacja i wybrać Nie sprawdzaj dostępności aktualizacji (niezalecane: zagrożenie bezpieczeństwa).

Można również skonfigurować Selenide, aby używał innej przeglądarki, na przykład Chrome, co skutecznie rozwiąże problem, o ile wymaganiem nie jest, aby testy musiały być uruchamiana także w Firefoxie.

Chrome i Linux

Po zmianie konfiguracji, aby domyślną przeglądarką był Chrome, w Windowsie Selenide (a dokładniej Selenium) zaraz po uruchomieniu przeglądarki maksymalizuje jej okno. Natomiast w systemie Linux (sprawdzałem w Mint 18.1) tego nie robi. Mimo tego, że domyślnie własność selenide.startMaximized jest ustawiona na true. Niestety ma to znaczenie, gdy nasza aplikacja webowa dostosowuje się do wielkości okna, w zależności od której serwowana jest odpowiednia treść. W ten sposób, ten sam test napisany w Windowsie, może nie zadziałać w Linuxie, bo Selenide nie może znaleźć elementu lub jest on niewidoczny, a na takim elemencie nie można wykonać akcji, np. kliknięcia. Aby się z tym uporać, najlepiej napisać własną klasę implementującą interfejs WebDriverProvider:

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.remote.DesiredCapabilities;

import com.codeborne.selenide.WebDriverProvider;

public class ChromeDriverMaximized implements WebDriverProvider {

	@Override
	public WebDriver createDriver(DesiredCapabilities desiredCapabilities) {
		ChromeDriver driver = new ChromeDriver(desiredCapabilities);
		driver.manage().window().maximize();
		return driver;
	}

}

a następnie wskazać na nią ustawiając właściwość w kodzie:

System.setProperty("selenide.browser", "specs.base.ChromeDriverMaximized");

lub jako parametr uruchomieniowy (argument VM):

-Dselenide.browser=specs.base.ChromeDriverMaximized

Podsumowanie

Teraz powinieneś być w stanie szybko i łatwo tworzyć testy automatyczne GUI. Selenide stanowi dobry wybór dla programistów, ponieważ nie wymaga dużo nauki – nie trzeba przekopywać się przez masę dokumentacji i samouczków, aby zacząć pracę, co jak wiadomo ma znaczenie. Niemal w naturalny sposób można napisać test i/lub go zrozumieć. Zasadniczo nie trzeba przejmować się rodzajem i szczegółami przeglądarki. Selenide w szybki i łatwy sposób rozwiązuje większość problemów związanych z testowaniem złożonych aplikacji frontendowych, w tym te dotyczące dynamicznych treści, Ajax i timeoutów. Jednak czasem nie obejdzie się bez pewnych haków, które będziemy musieli zastosować, aby test zadziałał; zazwyczaj sprowadza się to do wstawienia w paru miejscach metody sleep(long milliseconds)).

Wszystko to nie oznacza, że Selenium WebDriver jest złym narzędziem, ale po prostu narzędziem przeznaczonym do innego celu niż czyste testowanie. Dlatego szkoda czasu na customizowanie Selenium, skoro mam do tego dedykowane, gotowe narzędzie.


Łukasz Santarek

Full Stack Developer, konsultant IT.

Z wykształcenia elektronik, z zamiłowania informatyk. Interesuje się językami programowania, bazami danych oraz technikami i strategiami tworzenia oprogramowania. W pracy zajmuje się głównie Javą, a ostatnio również odnajduje się w tworzeniu aplikacji webowych, opartych głównie na Angularze. W wolnym czasie czyta beletrystykę i książki popularnonaukowe. Czasem można go spotkać w teatrze lub na leśnych szlakach.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Trwa ładowanie