Selenium WebDriver MindMap


This draft of a Selenium WebDriver MindMap was drawn with the wonderful https://bubbl.us/. The goal of this draft is to propose an overview of the main subjects while working with Selenium WebDriver and where it make sense to acquire knowledge sooner or later.
Integration
WebDriver is developed for the real world and therefore it depends on real projects with real setups. So a Selenium developer needs knowledge of these environments.
Agile Development
Web Browser Automation is an important part of agile development http://en.wikipedia.org/wiki/Agile_software_development
Cloud
www.saucelabs.com, www.cloudbees.com, …
Build-Tools
Maven, Ant, …
Continous Integration
The shorter the test cycles, the more Web Browser Automation pay off. CI-Server: Jenkins
WebDriver API
It’s the core of the Selenium engagement: http://selenium.googlecode.com/svn/trunk/docs/api/java/index.html
Selenium World
Knowing the history (Selenium 1 -> Selenium 2 = WebDriver), knowing the key players (e.g. Simon Stewart), knowing the knowledge base (http://seleniumhq.org/), knowing support ressources (https://groups.google.com/forum/?fromgroups#!forum/selenium-users), …
Pattern
There are design patterns, like Page Object-Pattern.
And there are recipes for recurrend problems, like “check if an element exists” (http://stackoverflow.com/questions/6521270/webdriver-check-if-an-element-exists)
Internals
Knowing the internals isn’t needed in straight forward problems. But it’s very helpful with complex problems and to develop sophisticated solutions.
Driver Internals
http://code.google.com/p/selenium/wiki/ArchitecturalOverview#A_Layered_Design
Browser Internals
Long time the rendering engines of browsers have been black boxes. Tali Garsiel helped a lot to reveal these internals: http://taligarsiel.com/Projects/howbrowserswork1.htm
Programming
Except for stumbeling with Selenium IDE, skills in programming are fundamental for Web Browser Automation with Selenium.
Programming Language
All programming languages with bindings to WebDriver API: http://seleniumhq.org/docs/03_webdriver.html#setting-up-a-selenium-webdriver-project
IDE
NetBeans, Eclipse, …
HTML
target of all the efforts
XPath
needed for sophisticated navigation; fantastic tutorial:
Test-Framework
TestNG, junit, …
Browser Tools
in the MindMap you find addons for my favorite development browser: Firefox.

Akkordeon-Menü mit Selenium

Beim Umgang mit einem Akkordeon-Menu (hier umgesetzt mit jquery – das Beispiel befindet sich am Ende des Artikels: http://viralpatel.net/blogs/create-accordion-menu-jquery/) ergibt sich bei der Testautomation das Problem, dass ich nicht einfach kodieren kann:
1.) Clicke auf “Technology”-Link.
2.) Clicke auf “Twitter”-Link.
um zur entsprechenden Seite zu gelangen. In einem komplexeren Testfall, bei dem ich vielmals mit dem Akkordeon-Menü arbeite, kommt es vor, dass das Technology-Menü bereits geöffnet ist. Dann würde ein Klicken das Menü wieder schließen und ich käme an den Twitter-Link nicht mehr ran – mein Testfall würde mit einer ElementNotVisibleException über den Jordan gehen. Daher müssen wir noch eine kleine Abfrage einbauen:
1.) Falls ein das Technology-Menü nicht geöffnet ist (=”Twitter”-Link nicht da ist), öffne es.
2.) Clicke auf Twitter-Link.
 
Abfragen, ob ein bestimmtes Element existiert, funktionieren im Webdriver nur indirekt, wie hier ausgeführt: http://stackoverflow.com/questions/6521270/webdriver-check-if-an-element-exists Um die Diskussion zusammen zu fassen (und zu ergänzen):
1.) Die eleganteste Lösung die Existenz eines Elements zu prüfen ist
driver.findElements( By.id("...") ).size() != 0
2.) Wenn man mit implicit wait arbeitet (wie im Folgenden angenommen), dann sollte man den Sekundenwert in einer Variable standardSecondsToWait speichern und zwar dort, wo man den implicit wait initial festlegt:
int standardSecondsToWait = 30;
driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);

3.) Der driver wartet die komplette Zeit des implicit wait, bis er endlich entscheidet, dass ein tatsächlich nicht vorhandenes Element auch tatsächlich nicht vorhanden ist. Erst dann entscheidet er, dass die o.g. Bedingung falsch ist und erst dann fährt er mit der Programmausführung fort. Bei mehreren solcher Prüfungen in einem Testcase und entsprechender Länge des implicit wait verschwenden wir kostbare Testzeit.
4.) Eine good practice ist es daher, die Bedingung isoliert und innerhalb von einer “implicit wait-Klammer” abzufrühstücken:

driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
boolean exists = driver.findElements( By.id("...") ).size() != 0
driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);
if (exists) {do something} (...)

Konsequenterweise umschließt die “implicit wait-Klammer” nur die Abfrage dieser einen Bedinungung und nicht mehr, denn wenn man ein ausgesprochener Fan der implicit waits ist (iGs zu den Fans der explicit waits), dann sollte man auch nur im notwendigen Fall davon abweichen.
Am Ende des ersten Absatzes sprach ich von der Prüfung, ob der “Twitter”-Link da ist. Der zweite Absatz handelt von der Prüfung, ob ein Element existiert. Genau gesagt geht es jedoch darum, ob ein Element im Akkordeon-Menü (im Beispiel der “Twitter”-Link) sichtbar ist. Die Prüfung auf Existenz (also ob der Knoten im DOM tree angelegt ist) greift zu kurz, denn ein existierendes Element könnte vom stylesheet noch display:none rangeklatscht bekommen: es existiert, ist jedoch nicht sichtbar. Genau das macht aber das Akkordeon-Menü aus: nur die Links des geöffneten Submenüs sind sichtbar – die Links der anderen Submenüs existieren zwar, sind jedoch unsichtbar (display:none). WebDriver quittiert den Klickversuch dieser unsichtbaren Links mit einer ElementNotVisibleException (gemäß dem Anspruch einen menschlichen User zu simulieren). Wir müssen den Ansatz im zweiten Absatz also noch anpassen:

driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        Boolean isOffen = driver.findElement(By.xpath("//a[contains(text(),'Football')]")).isDisplayed();
        driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);

Achtung: Würde ich hier mittels linkText() oder partialLinkText() suchen, statt mir xpath(), würde dieser Code nicht funktionieren. Diese beiden Methoden erfordern einen sichtbaren Text und wenn sie diesen nicht finden, brechen Sie den Testlauf mit “NoSuchElement” ab (http://www.seleniumhq.org/docs/03_webdriver.html#by-link-text)
Und hier der vollständige Code:

package de.it-kosmopolit.meinblog;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.internal.Locatable;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.Test;
public class Accordeon {
    @Test
    public void testAccordeon() throws InterruptedException, MalformedURLException {
        String username = "it-kosmopolit";
        String accessKey = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
        String url = "http://" + username + ":" + accessKey + "@ondemand.saucelabs.com:80/wd/hub";
        DesiredCapabilities cap = DesiredCapabilities.firefox(); //Browserdefaults auswählen
        cap.setCapability("version", "15"); //Version des Browsers festlegen
        cap.setCapability("platform", "Windows 2008"); //Betriebssystem festlegen
        cap.setCapability("username", username); //Mein Username bei SauceLabs
        cap.setCapability("accessKey", accessKey); //Mein Access-key bei SauceLabs
        WebDriver driver = new RemoteWebDriver(new URL(url), cap);
        int standardSecondsToWait = 30;
        driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);
        driver.get("http://viralpatel.net/blogs/create-accordion-menu-jquery/");
        // das Fenster an die Position scrollen, mit der man das "Schauspiel" am besten sieht
        // Code-Quelle: http://stackoverflow.com/a/9852744
        Locatable hoverItem = (Locatable) driver.findElement(By.xpath("//h2[contains(text(),'Online Demo')]"));
        int y = hoverItem.getCoordinates().getLocationOnScreen().getY();
        ((JavascriptExecutor) driver).executeScript("window.scrollBy(0," + y + ");");
        Thread.sleep(2000); //nur für's "Schauspiel"
        //das Beispiels-Akkordeon befindet sich in einem namenlosen iframe ...
        WebElement properFrame = driver.findElement(By.xpath("//iframe/preceding-sibling::br/following-sibling::iframe"));
        driver.switchTo().frame(properFrame);
        driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        Boolean isOffen = driver.findElement(By.xpath("//a[contains(text(),'Football')]")).isDisplayed();
        driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);
        //Das Sports-Menü ist initial tatsächlich geöffnet.
        //Ohne die Prüfung würde es hier also knallen (ElementNotVisibleException)!
        if (isOffen) {
            driver.findElement(By.xpath("//a[contains(text(),'Football')]")).click();
            Thread.sleep(2000); //nur für's "Schauspiel"
        } else {
            driver.findElement(By.xpath("//li[contains(text(),'Sports')]")).click();
            driver.findElement(By.xpath("//a[contains(text(),'Football')]")).click();
        }
        driver.manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS);
        Boolean isOffen2 = driver.findElement(By.xpath("//a[contains(text(),'Twitter')]")).isDisplayed();
        driver.manage().timeouts().implicitlyWait(standardSecondsToWait, TimeUnit.SECONDS);
        if (isOffen2) {
            driver.findElement(By.xpath("//a[contains(text(),'Twitter')]")).click();
        } else {
            driver.findElement(By.xpath("//li[contains(text(),'Technology')]")).click();
            Thread.sleep(2000); //nur für's "Schauspiel"
            driver.findElement(By.xpath("//a[contains(text(),'Twitter')]")).click();
            Thread.sleep(2000); //nur für's "Schauspiel"
        }
        driver.quit();
    }
}

Und hier der Testcase zum Anschauen: https://saucelabs.com/tests/e7cdafe4389b4585867399af71907ea2#
(Hinweis: die Links sind “blinde” Demolinks – führen also niergens hin, im Video erkennt man das Klicken an einer Farbänderung des Links von blau zu rot)

Selenium: Parallelisierung durch Parametrisierung

Im Blogbeitrag Parallelisierung von Selenium-Tests mittels Sauce Labs blieb ich noch eine skalierbare Parallelisierungslösung schuldig. TestNG bietet mit seinem DataProvider die Möglichkeit Parameter an seine Testmethoden zu übergeben (http://testng.org/doc/documentation-main.html#parameters-dataproviders). Dieses Tool möchte ich nun nutzen, um meinen Testmethoden zu sagen, welche Browser sie für meine Seleniumtests verwenden sollen. Als “Browserpark” nutze ich wieder mal die Cloud der Sauce Labs Inc.
Den DataProvider kann man auch außerhalb der Testklasse speichern und dann in jede Testklasse integrieren. Meist sind bei den Funktionstests zahlreiche Testklassen im Spiel. Da ich nicht bei jedem major release von Firefox und Konsorten in jede Testklasse einzeln gehen möchte, um diesen dort hinzuzufügen, drängt sich Auslagerung des DataProviders in eine eigene .class geradezu auf.
Die verwendeten Browser sind:

  1. Firefox, Version 12, Linux
  2. Internet Explorer, Version 9, Windows Vista
  3. Chrome, Version von Sauce Labs bestimmt, Windows Vista
  4. Safari, Version 5, Mac

Ich verwende im Beispiel also zwei Testklassen namens Parallel1Test.java und Parallel2Test.java, sowie eine Klasse worin ich den DataProvider auslagere, die ich, angelehnt an einen Fuhrpark, mit BrowserPark.java bezeichne.
Parallel1Test.java

import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.Assert;
import org.testng.annotations.Test;
public class Parallel1Test {
    @Test(description = "Testet Erreichbarkeit meiner Angebotsseite.", dataProvider = "BrowserPark", dataProviderClass = BrowserPark.class)
    public void paralleltest1(URL url, DesiredCapabilities caps) throws MalformedURLException, InterruptedException {
        //Konfigurationen
        caps.setCapability("name", "paralleltest1: check Michael Wowro");
        WebDriver driver;
        if (BrowserPark.isDevelopedLocal) driver = new FirefoxDriver(caps);
        else driver = new RemoteWebDriver(url, caps);
        driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
        //Testcase
        driver.get("https://www.it-kosmopolit.de");
        driver.findElement(By.linkText("KONTAKT UND ANGEBOT")).click();
        WebElement profil = driver.findElement(By.partialLinkText("xing"));
        Assert.assertTrue(profil.getText().contains("Michael_Wowro"));
        //Beenden
        driver.quit();
    }
}

Parallel2Test.java

import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.Assert;
import org.testng.annotations.Test;
public class Parallel2Test {
    @Test(description = "Testet Erreichbarkeit meiner Angebotsseite.", dataProvider = "BrowserPark", dataProviderClass = BrowserPark.class)
    public void paralleltest2(URL url, DesiredCapabilities caps) throws MalformedURLException, InterruptedException {
        //KonfigurationeN
        caps.setCapability("name", "paralleltest2: check xing");
        WebDriver driver;
        if (BrowserPark.isDevelopedLocal) driver = new FirefoxDriver(caps);
        else driver = new RemoteWebDriver(url, caps);
        driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
        //Testcase
        driver.get("https://www.it-kosmopolit.de");
        driver.findElement(By.linkText("KONTAKT UND ANGEBOT")).click();
        WebElement profil = driver.findElement(By.partialLinkText("xing"));
        Assert.assertTrue(profil.getText().contains("xing"));
        //Beenden
        driver.quit();
    }
}

browserPark.java

import java.net.MalformedURLException;
import java.net.URL;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.testng.annotations.DataProvider;
// Übersicht über Browser-Angebot bei Sauce Labs: https://saucelabs.com/docs/browsers
public class BrowserPark {
    static boolean isDevelopedLocal;
    static String username = "it-kosmopolit";
    static String accessKey = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
    static String url = "http://" + username + ":" + accessKey + "@ondemand.saucelabs.com:80/wd/hub";
//    /* Entwicklung gegen meinen lokalen Firefox-Browser */
//    @DataProvider(name = "BrowserPark")
//    public static Object[][] developLocal() throws MalformedURLException {
//        isDevelopedLocal = true;
//        DesiredCapabilities capFF = DesiredCapabilities.firefox(); //Browserdefaults auswählen
//
//        return new Object[][]{
//                    {new URL("http://www.egal.de"), capFF}
//        };
//    }
//    /* Entwicklung gegen einen ausgesuchten Browser in der Sauce Labs-Cloud */
//    @DataProvider(name = "BrowserPark")
//    public static Object[][] developRemote() throws MalformedURLException {
//        isDevelopedLocal = false;
//
//        // FF
//        DesiredCapabilities capFF = DesiredCapabilities.firefox(); //Browserdefaults auswählen
//        capFF.setCapability("version", "12"); //Version des Browsers festlegen
//        capFF.setCapability("platform", "Linux"); //Betriebssystem festlegen
//        capFF.setCapability("username", username); //Mein Username bei SauceLabs
//        capFF.setCapability("accessKey", accessKey); //Mein Access-key bei SauceLabs
//
//        return new Object[][]{
//                    {new URL(url), capFF}
//        };
//    }
    /* Produktivmodus mit den von meinen Kunden gewünschten Browser-Betriebssystem Kombinationen */
    @DataProvider(name = "BrowserPark")
    public static Object[][] productive() throws MalformedURLException {
        isDevelopedLocal = false;
        // FF
        DesiredCapabilities capFF = DesiredCapabilities.firefox(); //Browserdefaults auswählen
        capFF.setCapability("version", "12"); //Version des Browsers festlegen
        capFF.setCapability("platform", "Linux"); //Betriebssystem festlegen
        capFF.setCapability("username", username); //Mein Username bei SauceLabs
        capFF.setCapability("accessKey", accessKey); //Mein Access-key bei SauceLabs
        // IE
        DesiredCapabilities capIE = DesiredCapabilities.internetExplorer(); //Browserdefaults auswählen
        capIE.setCapability("version", "9"); //Version des Browsers festlegen
        capIE.setCapability("platform", "Windows 2008"); //Betriebssystem festlegen
        capIE.setCapability("username", username); //Mein Username bei SauceLabs
        capIE.setCapability("accessKey", accessKey); //Mein Access-key bei SauceLab
        //Chrome
        DesiredCapabilities capCH = DesiredCapabilities.chrome(); //Browserdefaults auswählen
        capCH.setCapability("platform", "Windows 2008"); //Betriebssystem festlegen
        capCH.setCapability("username", username); //Mein Username bei SauceLabs
        capCH.setCapability("accessKey", accessKey); //Mein Access-key bei SauceLab
        //Safari
        DesiredCapabilities capSA = DesiredCapabilities.safari(); //Browserdefaults auswählen
        capSA.setCapability("version", "5");//Version des Browsers festlegen
        capSA.setCapability("platform", "Mac 10.6");//Betriebssystem festlegen
        capSA.setCapability("username", username); //Mein Username bei SauceLabs
        capSA.setCapability("accessKey", accessKey); //Mein Access-key bei SauceLab
        return new Object[][]{
                    {new URL (url), capFF},
                    {new URL (url), capIE},
                    {new URL (url), capCH},
                    {new URL (url), capSA}
        };
    }
}

Wie im Code zu sehen, habe ich drei Versionen des DataProviders:

  1. zum Entwickeln der Tests gegen meinen aktuell lokal installierten Firefox
  2. zum Entwickeln der Tests gegen einen ausgewählten Browser in der Sauce Labs-Cloud
  3. und den vom Kunden gewünschten Browserpark, gegen den seine Internet-Anwendung getestet wird (sozusagen der Produktivmodus). Und hier kann man sich die Browser-Betriebssystem-Kombinationen für seinen individuellen Browserpark zusammenstellen: https://saucelabs.com/docs/browsers

Die beiden Versionen, die ich gerade nicht nutzen möchte, schalte ich einfach per Auskommentieren aus.
Alle 3 Klassen liegen in meiner Maven-Verzeichnisstruktur \src\test\java. Ein entsprechendes Maven-Projekt habe ich in netbeans angelegt. Ein Click auf “test” und nachdem mir netbeans den erfolgreichen Testlauf bestätigt, gehe ich in mein Sauce Labs-Konto um mir die Schätzchen anzuschauen. Und siehe da, für jeden meiner vier verschiedenen Browser sind je beide Tests erfolgreich durchgelaufen:

Hinweis: Da das für diesen Blogpost verwendete Sauce Labs-Konto auf zwei Threads beschränkt ist, sind in diesem Beispiel immer nur zwei Testläufe parallel.
Weil ich es richtig gut finde, dass Sauce Labs von jedem meiner Testläufe ein Video aufzeichnet, schau ich mir eins auch noch mal an: