Blog

Testdrive your WebApplication with Selenium WebDriver and PageObjects (Teil 1)

Sucht man nach Möglichkeiten zum Testen von Webanwendungen, landet man relativ schnell bei Selenium, einem Framework, das vielen Anwendungsentwicklern bekannt sein dürfte. Hierbei werden Tests klassisch via capture & replay aufgezeichnet und wieder zum Ablauf gebracht. Dazu kann z.B. im Firefox das Selenium-IDE-Plugin verwendet werden. Die aufgezeichneten Tests werden standardmässig im HTML-Format gespeichert, können jedoch auch direkt in JUnit-Tests exportiert werden.

Da bei dieser Variante kaum mehr eigenhändiges Codieren der Tests notwendig ist, eignet sich dieses Verfahren für Szenarios, bei denen Tester nicht programmieren können oder sollen. Erfahrungsgemäß bleibt der Einstieg in den Code aber auch bei diesem Szenario nicht erspart, denn mitunter müssen noch wait() statements eingebaut werden, weil die Antwortszeiten zwischen den Rechnern der Tester und der Integrationsumgebung doch stark abweichen können.

Obwohl die Erstellung und Pflege von Seleniumtests dank des Toolings recht einfach sind und keine gravierenden Programmierkenntnisse erfordern, kann jedoch der Aufwand zur Einrichtung einer Integrationsumgebung recht aufwändig und zeitintensiv sein. Um Tests automatisiert ausführen zu können, muss zunächst die zu testende Anwendung deployed und darüberhinaus ein Client konfiguriert werden, der die Tests gegen die zu testende Anwendung ausführt. Dazu wird in der Regel eine Umgebung mit grafischer Oberfläche benötigt, in der vom Testclient die Testfälle in einem Browser ausgeführt werden. Allerdings ist dies bei Integrationsumgebungen nicht immer gegeben – insbesondere, wenn beispielsweise auf Windows-Rechnern entwickelt und getestet wird und sich die Integrationsumgebung auf Linuxsystemen ohne grafische Oberfläche befindet. Selenium sieht für diesen Fall die Einrichtung eines Virtual Framebuffers vor, in dem die Tests dann virtualisiert ausgeführt werden, ohne dass wirklich eine grafische Oberfläche angezeigt wird. Allerdings ist die Konfiguration nicht ganz trivial, und nicht selten weicht man dann auf separate Maschinen aus, die eigens für die Ausführung der Tests konfiguriert sind.

Der entscheidende Nachteil beim klassischen Einsatz von Seleniumtests ist aus meiner Sicht jedoch die Tatsache, dass lediglich das gegenwärtig implementierte Verhalten fixiert wird. Dabei ist jedoch nicht sicher, ob dies auch den Anforderungen bzw. Akzeptanzkriterien genügt. Es können lediglich Probleme aufgedeckt werden, die eventuell durch spätere Änderungen am Code entstehen und andererweitige Funktionalitäten in Mitleidenschaft ziehen. Im Sinne eines strikt testgetriebenen Vorgehens wäre es natürlich wünschenswert, solche Akzeptanztests anhand entsprechender Kriterien vorab zu formulieren. Somit dokumentieren die Tests die Akzeptanzkrierien vollständig, zeigen nicht nur fehlerhafte, sondern auch nicht abgedeckte Funktionalitäten und erlauben somit zu jeder Zeit eine belegbare Aussage über den Entwicklungsstand des Programmcodes.

Selenium bietet mit WebDriver zusätzlich ein interessantes API, um derartige Tests zu formulieren und komplett headless ausführen zu können. Prinzipiell ist WebDriver stark an das Framework HttpUnit angelehnt, unterstützt aber unter anderem auch die Ausführung von JavaScript.

Auch beim Testen mit der API muss die zu testende Anwendung natürlich deployed werden. Im Rahmen dieses Beispiels wird dazu im Setup der JUnit-Tests ein embedded Jetty als Servlet-Container gestartet und die Anwendung direkt aus dem Projektverzeichnis heraus auf diesem ausgeführt.

public abstract class AbstractWebTestTemplate {

	private Server server

	private HtmlUnitDriver webDriver;

	protected abstract String getDescriptor();

	protected abstract String getWebApp();

	protected abstract String getContext();

	protected abstract Integer getPort();

	protected abstract String getHost();

	@Before
	public void setup() throws Exception {
		WebAppContext context = new WebAppContext(getWebApp(), getContext());
		context.setParentLoaderPriority(true);

		server = new Server(getPort());
		server.setHandler(context);
		server.start();

		assertTrue(server.isRunning());

		webDriver = new HtmlUnitDriver();
	}

	@After
	public void tearDown() throws Exception {
		server.stop();
	}

Nach Start des Servlet-Containers und dem Deployment der Anwendung wird eine Instanz der Klasse WebDriver erzeugt. Diese wird dann verwendet, um die zu testende Anwendung aus JUnit heraus zu steuern. Zunächst wird dazu eine URL aufgerufen, unter der die zu testende Anwendnung zu erreichen ist:

WebDriver driver = new HtmlUnitDriver();
driver.get(“http://localhost:9090/webdriver-demo/”);

Das resultierende HTML wird an die WebDriver-Instanz zurückgegeben und kann via driver.getPageSource(); komplett ausgelesen werden. Um gezielten Zugriff auf bestimmte Seiteninhalte zu ermöglichen, bietet das API zusätzliche Methoden. Folgendes Beispiel demonstriert, wie der Login-Vorgang in einem automatisierten Test mit WebDriver aussehen könnte:

public class LoginTest extends AbstractWebTest {

	@Test
	public void testLoginSuccessViaWebDriver() throws Exception {
		getWebDriver().get(“http://localhost:9090/webdriver-demo/login.do”);
		try {
			getWebDriver().findElement(By.id(“username”)).sendKeys(“user”);
			getWebDriver().findElement(By.id(“password”)).sendKeys(“password”);
			getWebDriver().findElement(By.id(“login”)).submit();
			String h1 = getWebDriver().findElement(By.tagName(“h1”)).getText();
			assertEquals(“Welcome”, h1);
		} catch (NoSuchElementException ex) {
			/*
			 * in case the element couldn't be found, i.e. an error occured,
			 * catch the according exception and print the actual page source
			 */
			fail(String.format(“%sn%s”, ex.getMessage(), getWebDriver().getPageSource()));
		}
	}

Nach Aufruf der Login-URL werden mittels der findElement()-Methoden die entsprechenden Form-Elemente gesucht, um die Login-Informationen anzugeben. In diesem Fall geschieht dies explizit über die IDs der HTML-Elemente. Möglich sind aber auch andere Strategien wie name, tagname oder auch via XPath-Expression. Werden die korrespondierenden Seitenelemente nicht gefunden, wird eine NoSuchElementException geworfen. Im vorliegenden Fall wird diese gefangen und die ID sowie der Seitenquelltext im Ergebnis des JUnit-Tests ausgegeben. Da noch keine Seite mit den angegeben Form-Elementen und auch kein Servlet für die Behandlung des Requests implementiert wurden, schlägt der Test mit entsprechender Fehlermeldung fehl.

Um den Test zu fixen, werden nun die entsprechenden Seiten angelegt, ein Servlet als Handler für den Request implementiert und registriert. Im Beispiel wurde dazu das Spring-MVC-Framework eingesetzt und für die Behandlung des Requests ein Controller implementiert, der nach Eingabe der Logindaten zunächst die Willkommensseite anzeigt.

<%@ page language=“java” contentType=“text/html; charset=ISO-8859-1”
 pageEncoding=“ISO-8859-1”%>
<!DOCTYPE html PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN” “http://www.w3.org/TR/html4/loose.dtd”>
<html>
<head>
<meta http-equiv=“Content-Type” content=“text/html; charset=ISO-8859-1”>
<title>Login</title>
</head>
<body>
 <h1>Login</h1>
 <form action=“login.do” method=“post”>
 <input id=“username” type=“text” />
 <input id=“password” type=“password” />
 <input id=“login” type=“submit” value=“anmelden”/>
 </form>
</body>
</html>
package de.holisticon.webdriver.demo.web.controller;

import java.io.IOException;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(“/login.do”)
public class LoginController {

	@RequestMapping(method = RequestMethod.GET)
	public String handleGet() throws IOException {
		return Form.LOGIN;
	}

	@RequestMapping(method = RequestMethod.POST)
	public String handlePost() throws IOException {
		/*
		 * simply show the welcome page for the first test
		 */
		return Form.WELCOME;
	}
}


So können weitere Funktionalitäten ergänzt werden, indem zunächst die Tests geschrieben werden und anschließend dagegen implementiert wird. Wie anhand des Snippets zu erkennen ist, können die Tests bei komplexeren Szenarios schnell sehr unleserlich werden und den eigentlichen Workflow verdecken. Wie dies optimiert werden kann, wird im zweiten Teil vorgestellt.

Holisticon AG — Teile diesen Artikel

Über den Autor

Avatar

Roland Jülich arbeitet als Java Consultant im Geschäftsfeld Architektur der Holisticon AG in Hamburg. Während der vergangenen Jahre war er in verschiedenen Softwareprojekten bei Grossunternehmen und Behörden als Coach, Developer und Scrum Master tätig. Seine Schwerpunkte liegen hauptsächlich im Bereich agilen Entwicklungsmethoden wie BDD, TDD, XP und Clean Code. Er ist Co-Organizer der Softwerkskammer Hamburg einer lokalen Community der SoftwareCraftsmanship Bewegung in Deutschland.

Antwort hinterlassen