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

Parallelisierung von Selenium-Tests mittels Sauce Labs

Quickstart
Voraussetzungen
– lauffähiges Maven: http://maven.apache.org
– Account bei SauceLabs: https://saucelabs.com/
1.) irgendwo den Ordner it-kosmopolit anlegen, darunter den Ordner src, darunter den Ordner test, darunter den Ordner java
2.) ParallelTest.java in it-kosmopolit\src\test\java legen (credentials im code anpassen, siehe unten)

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", "it-kosmopolit"); //Mein Username bei SauceLabs
		caps.setCapability("accessKey", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"); //Mein Access-key bei SauceLabs
        RemoteWebDriver driver = new RemoteWebDriver(
                new URL("http://it-kosmopolit: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", "it-kosmopolit"); //Mein Username bei SauceLabs
		caps.setCapability("accessKey", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"); //Mein Access-key bei SauceLabs
        RemoteWebDriver driver = new RemoteWebDriver(
                new URL("http://it-kosmopolit: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();
    }
}

3.) pom.xml in it-kosmopolit legen

<?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>
    <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>
                <configuration>
                    <skip>true</skip>
					<parallel>methods</parallel>
					<threadCount>10</threadCount>
                </configuration>
                <executions>
                    <execution>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <skip>false</skip>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.) in der DOS-Box in it-kosmopolit gehen und Folgendes ausführen: C:\“Program Files“\apache-maven-3.0.x\bin\mvn integration-test
Testcase
1.) Geh auf meinen Blog http://itkosmopolit.wordpress.com/
2.) Klicke dort auf den Link „KONTAKT UND ANGEBOT“
3.) Wenn der Link zum XING-Profil den Linktext „Michael_Wowro“ enthält, dann OK.
Story
Gerade wenn man die Funktionstests in seinen Continuous Integration einbinden will, jedoch auch, wenn man diese als Regressionstests vor jedem Release nutzt, kommt es auf die Durchführungsgeschwindigkeit der Tests an. Das Konzept schlechthin, die Funktionstests zu beschleunigen ist die Parallelisierung. Ich wähle für diesen Blogbeitrag Mozilla Firefox (auf Linux) und Internet Explorer (auf Windows) als Selenium Browser aus. Als Testframeworks werden in der Selenium-Community sowohl JUnit, als auch TestNG verwendet. TestNG erfreut sich jedoch größerer Beliebtheit (http://sauceio.com/index.php/2010/10/parallel-junit-4-and-selenium-part-one-parameters), weshalb ich es im Folgenden verwende. Für die Parallelisierung auf Methodenebene sorgt Maven und benötigt hierfür nur zwei Zeilen (siehe auch http://maven.apache.org/plugins/maven-surefire-plugin/examples/testng.html):

Das gleichzeitige Ausführen eines Selenium-Tests auf zwei verschiedenen Browsern (Parallelisierung) auf einem Rechner erweist sich als instabil. Manchmal funktioniert’s, manchmal gibt’s ein Unable to find element:

Diese Diskussion bestätigt dieses Phänomen: http://www.seleniumwebdriver.com/selenium-webdriver-developers/webdriver-directed-firefox-instances-fighting-each-other-on-the-same-machine/
Da ich mir das Aufsetzen eines Selenium Grids sparen möchte, gehe ich gleich zur Cloud-Lösung = Sauce Labs über: http://sauceio.com/index.php/tag/parallel-testing/ Voraussetzung ist hierfür natürlich ein Konto bei SauceLabs, welches jedoch einfach und ohne Verzögerung anzulegen ist. Für die Nutzung der SauceLabs API durch unser Testprogramm sind lediglich ein Username (wählt man selbst bei Anlegen des Kontos) und ein API-key (synonym: Access-key) nötig. Letzteren erzeugt SauceLabs automatisch und man kann diesen im Konto einsehen (und dort auch neu generieren lassen):
Dem besseren Verständnis geschuldet, baue ich das Testprogramm ohne Parametrisierung, also mit je einer eigenen Testmethode für den Firefox und einer für den Internet Explorer.
Die von SauceLabs angebotenen Browser-Betriebssystem-Kombis findet man hier: http://saucelabs.com/docs/ondemand/browsers – dort kann man auch direkt den Code für die gewünschte Programmiersprache generieren lassen. In diesem Blogbeitrag wird Java verwendet.
An dieser Stelle ist die SauceLabs-Doku etwas schlampig und die Eingabe der Credentials redundant. Neben der Verwendung des Usernames und des Access-Keys in der url, muss man diese auch noch in den capabilities aufrufen:

capabillities.setCapability("username", "it-kosmopolit");
capabillities.setCapability("accessKey", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
RemoteWebDriver driver = new RemoteWebDriver(
new URL("http://it-kosmopolit:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"@ondemand.saucelabs.com:80/wd/hub]]"),
capabillities);

Beachtet man dies nicht, wird man mit folgender Fehlermeldung „belohnt“:

"java.lang.UnsupportedOperationException: Unknown username.
You sent username 'None' in your browser string, which is not a valid Sauce Labs account."

Siehe auch: http://saucelabs.com/forums/viewtopic.php?id=509
Wenn alles gut geht, dann erscheint in der DOS-Box die Meldung mit Failures: 0 und Errors: 0

Unter myTests im SauceLab-Account kann man sich die (von mir erzeugten) Testergebnisse als Video und Log anschauen

Wenn man sich nun die Endzeiten (Finished) und die Laufzeit (Duration) anschaut, erkennt man die Parallelität der Testausführung. q.e.d.
Die Testreports habe ich auf public gesetzt – daher kann nun jeder, der die url kennt (=Datensicherheit) sich diese nun anschauen:
Internet Explorer
Firefox
Die Videos sind bedingt durch die Einfachheit der Testcases natürlich nicht besonders spektakulär – die Möglichkeiten, die damit gezeigt werden jedoch schon …
Spätestens, wenn man sich die Übersichtlichkeit der Testreports anschaut, erkennt man, dass die Jungs von SauceLabs einen tollen Job machen.

Parametrisierte Google Suche

Motivation
Ein wichtiges Instrument eines Webscrapers sind tatsächlich URLs, denn damit landet man immerhin schon mal auf der gewünschten Seite (manchmal auch schon in den gewünschten Ergebnissen). Im Kontext der Google Suche kann die gezielte Verwendung von URLs auch für einen Otto-Normal-Nutzer eine Arbeitserleichterung sein. Hat man regelmäßige gleichlautende Abfragen, speichert man diese sinnvollerweise als URL in seinen Favoriten/Bookmarks/Lesezeichen.
Anatomie der Google Suche URL
die URL beginnt immer mit
https://www.google.com/search?
dahinter werden die Parameter in beliebiger Reihenfolge angehängt
parametername=parameterwert
mehrere Parameter werden mit & zusammengefügt, sodass die URL letzendlich diese Form annimmt
https://www.google.com/search?parametername1=parameterwert1¶metername2=parameterwert2¶metername3=parameterwert3
Die Parameter
Da Google keine offizielle Spezifikation seiner Parameter rausgibt, muss man sich diese von verschiedenen Seiten zusammenklauben (bzw. selbst herausfinden):
Google Search URL Parameters – Query String Anatomy
The Ultimate Guide to the Google Search Parameters
Google search parameters in 2012
Eine kleine vereinfachte Tabelle für unser u.g. Beispiel

Parameter Bedeutung
q=suchbegriff1+suchbegriff2 Das sind die Suchbegriffe, die in das google-Suchfeld eingetragen werden. Die einzelnen Suchbegriffe werden mit + aneinandergefügt.
as_eq=suchbegriff3+suchbegriff4 Das sind die Suchbegriffe, die von der Google Suche ausgeschlossen werden. Die einzelnen Suchbegriffe werden mit + aneinandergefügt.
tbs=qdr:d tbs=qdr filtert die Ergebnisse in einem bestimmten Zeitraum. d bedeutet hier Tag, d.h. nur die Suchergebnisse der letzten 24 h werden angezeigt (w bedeutet week usw.). Gerade wenn man regelmäßige Suchanfragen hat, möchte man ja nicht immer das Gleiche angezeigt bekommen, sondern nur die neuesten (noch nicht gelesenen) Suchergebnisse. Für solche Zwecke ist dieser Parameter natürlich Gold wert.
tbs=qdr:d,sbd:1 Wenn man dann noch die Ergenisse nicht nach Relevanz (ein Kriterium, das bei kleinen Zeiträumen eh keine Rolle spielt), sondern nach Zeit sortiert haben mag, hängt man noch ,sdb:1 an.
num=100&as_qdr=all num bedeutet die Anzahl der Suchergebnisse pro Seite. Seitdem Google jedoch Google Instant eingeführt hat, funktioniert dieser Parameter alleine nicht mehr. Man muss zusätzlich mittels as_qdr=all Google Instant ausschalten. Ein ausführlicher Artikel hierzu: num Parameter trotz Instant

Ein Beispiel
Als Beispiel nehme ich die Suchbegriffe „Google“ und „Suche“ – vielleicht wird dieser Artikel dereinst auf der ersten SERP stehen … naja … als auszuschließende Begriffe nehme ich willkürlich „Wikipedia“ und „Apple“ – im Suchfeld erreicht man den Ausschluss durch vorangestelltes Minuszeichen.

Die Google Suche URL sieht dann entsprechend so aus (kopiert die URL in Euer Browser-Adressfeld und seht die Magie …)
https://www.google.com/search?q=Google+Suche&as_eq=Wikipedia+Apple&tbs=qdr:d,sbd:1&num=100&as_qdr=all
Und dann wird diese URL schließlich noch als Favorit/Bookmark/Lesezeichen abgespeichert – feddisch.

Anpacken!