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:

Selenium ganz in der Cloud

Meine bisherigen Blogbeiträge haben nur die Testausführung in die Cloud (saucelabs.com) geschickt (also sozusagen den “Tanz” mit den Browsern”). Der Seleniumcode residierte dabei auf meinem lokalen Rechner und wurde dort auch ausgeführt. Wie wäre es nun, wenn ich als Selenium-Dienstleister meinem Kunden einfach nur ein “Testbutton im Internet” bereitstellen müsste, damit er seine Tests zum Fliegen bringt und dort dann anschließend auch das Testergebnis zu sehen bekommt. All dies kann man beispielsweise mit cloudbees.com umsetzen, wo Kohsuke Kawaguchi, der Papa von Jenkins (und Hudson) inzwischen angestellt ist. Dort bekommt man beispielsweise eine vorkonfigurierte Jenkins-Instanz, eine Maveninstallation, diverse Repositories usw. – alles als Platform as a Service in der Cloud.
In diesem Blogpost verwendete Tools: Cloudbees DEV@cloud (svn-repository, Jenkins, Sauce Labs OnDemand), Maven, TestNG, Subversion, TortoiseSVN
In diesem Blogpost verwendeter Code:
Maven – pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>de.itkosmopolit.blog</groupId>
    <artifactId>paralleltest</artifactId>
    <version>1.0-SNAPSHOT</version>
	 <properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	 </properties>
    <dependencies>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>5.13.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium</artifactId>
            <version>2.0b1</version>
        </dependency>
	</dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
				<version>2.12.3</version>
                <configuration>
					<parallel>methods</parallel>
					<threadCount>2</threadCount>
                </configuration>
            </plugin>
		</plugins>
    </build>
</project>

ParallelTest.java (Selenium-Code)

package de.itkosmopolit.blog;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.By;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.Test;
import org.testng.Assert;
import java.net.URL;
import java.net.MalformedURLException;
public class ParallelTest {
    @Test
    public void parallel_with_firefox() throws MalformedURLException{
		//Konfigurationen
		DesiredCapabilities caps = DesiredCapabilities.firefox(); //Browser auswählen
        caps.setCapability("version", "13"); //Version des Browsers festlegen
        caps.setCapability("platform", "Linux"); //Betriebssystem festlegen
		caps.setCapability("name", "parallelTestFirefox"); //hier wird der Test benannt und in SauceLabs wieder auffindbar.
		caps.setCapability("username", "cloudbees_it-kosmopo"); //Mein Username bei SauceLabs
		caps.setCapability("accessKey", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"); //Mein Access-key bei SauceLabs
        RemoteWebDriver driver = new RemoteWebDriver(
                new URL("http://cloudbees_it-kosmopo:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx@ondemand.saucelabs.com:80/wd/hub"),
                caps);
		//Testcase
        driver.get("https://www.it-kosmopolit.de");
        driver.findElementByLinkText("KONTAKT UND ANGEBOT").click();
		WebElement profil = driver.findElement(By.partialLinkText("xing"));
		Assert.assertTrue(profil.getText().contains("Michael_Wowro"));
		//Beenden
        driver.quit();
    }
	 @Test
    public void parallel_with_internetExplorer() throws MalformedURLException {
	    //Konfigurationen
		DesiredCapabilities caps = DesiredCapabilities.internetExplorer(); //Browser auswählen
        caps.setCapability("version", "9"); //Version des Browsers festlegen
        caps.setCapability("platform", "Windows 2008"); //Betriebssystem festlegen
		caps.setCapability("name", "parallelTestInternetExplorer"); //hier wird der Test benannt und in SauceLabs wieder auffindbar.
		caps.setCapability("username", "cloudbees_it-kosmopo"); //Mein Username bei SauceLabs
		caps.setCapability("accessKey", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"); //Mein Access-key bei SauceLabs
        RemoteWebDriver driver = new RemoteWebDriver(
                new URL("http://cloudbees_it-kosmopo:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx@ondemand.saucelabs.com:80/wd/hub"),
                caps);
		//Testcase
        driver.get("https://www.it-kosmopolit.de");
        driver.findElementByLinkText("KONTAKT UND ANGEBOT").click();
		WebElement profil = driver.findElement(By.partialLinkText("xing"));
		Assert.assertTrue(profil.getText().contains("Michael_Wowro"));
		//Beenden
        driver.quit();
    }
}

Hier ist ein Quickstart, den ich als Ausgangspunkt für meinen Post genutzt habe: http://wiki.cloudbees.com/bin/view/DEV/Sauce+OnDemand+Service. Und noch ein Quickstart-Video: http://www.youtube.com/watch?v=NENNAZQxK28&feature=youtu.be
1.) Also brauchen wir ein Konto bei cloudbees, welches einfach anzulegen und in der Variante für unter 300 Minuten/Monat kostenlos ist: https://www.cloudbees.com/signup

Das was wir dort unter Domain/Account eingeben ist Teil der URL, mit der wir nach erfolgreicher Anmeldung unsere Jenkins-Instanz direkt erreichen können: https://[accountname].ci.cloudbees.com
Alternativ erreichen wir diese in unserem cloudbees-Konto mit Click auf den Builds-Button in der Navigationsleiste

2.) Im nächsten Schritt abonnieren wir den Service: Sauce Labs (s.u. Abbildung) – Achtung, hierbei wird automatisch ein neues (!) Sauce Labs Konto erstellt mit eigenen Credentials.

3.) Nun legen wir ein Subversion-Repository in cloudbees an – (Repositories-Button in der Navigationsleiste)
Von cloudbees aus betrachtet sieht das Ergebnis dann so aus:

4.) In diesem Repository hab ich per TortoiseSVN die pom.xml (/pom.xml) und die ParallelTest.java (/src/test/java/ParallelTest.java) importiert und zwar, wie in den Klammern gezeigt, in der Standard-Maven Verzeichnisstruktur.
5.) Als letzter Schritt bleibt noch, den Jenkins-Job anzulegen
5.1) Wir tragen den Projektnamen ein und wählen Maven 2 / 3 Projekt aus.
5.2) In der Rubrik “Source Code Management” wählen wir Subversion und tragen die URL unseres cloudbee-repositories ein.

5.3) Als Build-Auslöser habe ich “Build when a change is pushed to CloudBees Forge” angehakt. Dadurch startet der Build automatisch, sobald ich ins cloudbees Subversion-Repository committe. Das funktioniert bei cloudbees zuverlässig, wenn auch ein paar Sekunden zeitverzögert, da erst ein Build-Prozess frei werden muss. Selbst wenn ich die Seleniumtests lokal entwickle und auch lokal überprüfe, ob diese fehlerfrei durchlaufen, so kann ich mit dieser Konfiguration doch nachvollziehbar dokumentieren, dass meine Tests zum Zeitpunkt des commits fehlerfrei durchgelaufen sind.
5.4) In der Rubrik “build” verweise ich auf die pom.xml (wie bereits voreingestellt) und trage als Maven-Phase test ein.

[Exkurs: Falls die Entwickler meines Kunden Maven als Buildtool nutzen, kann ich Ihnen meine Tests auch als jar in cloudbees Maven-reposity zur Verfügung stellen: http://maven.apache.org/guides/mini/guide-attached-tests.html. Aber Ziel dieses Blogposts ist ja, meinem Kunden einen “Testbutton im Internet” zur Verfügung zu stellen. Also weiter …]
5.5) Den Rest lassen wir bei den default-Einstellungen. Die Einstellungen können natürlich über “Konfigurieren” jederzeit geändert werden.
FERTIG – etwas Aufwand, aber nun können wir unserem Kunden einen “Test-Button im Internet” geben, mit dem er seine Tests rennen lassen kann. Was er braucht, ist (abgesehen von Account-Credentials) nur die URL des Jenkins-Jobs (https://[accountname].ci.cloudbees.com/job/[jobname]).
Im Build-Verlauf erkennt er, ob die letzten “Builds” (bei unserer Verwendung von Jenkins vielleicht besser als “Testläufe” zu bezeichnen) erfolgreich waren oder nicht.

blau = Testlauf war fehlerfrei
gelb = Testlauf war fehlerbehaftet
Mit diesem Button startet er seine Seleniumtests:

Wenn’s geknallt hat, kann er auf den entsprechenden Build clicken und schauen, wo es genau geknallt hat:

Das dazugehörige Log kann mein Kunde natürlich auch einsehen und die entsprechenden SauceLabs-Videos (unter Services -> Sauce Labs).
Und wieder habe ich diese Videos für die Öffentlichkeit freigegeben, welche aufgrund der Einfachheit der Testcases nicht spektakulär daherkommen:
parallelTestInternetExplorer
parallelTestFirefox