Filed under: Java, Selenium, Test automation, — Tags: Cargo, Integration test, JUnit, Jetty, Maven, Spring, TDD, Web application — Thomas Sundberg — 2009-04-17
We want to build a web application and we want to test it automatically.
One solution is to make sure that whenever we perform an integration test, the application is deployed on a servlet container, a Selenium server is started and the application is verified through a web browser. That is automating the deployment process and the testing process. No person should start a browser, fill out a form and verify that the result is the expected.
Lets start with this Maven project structure:
src -------+- main ----+- java | | | +- webapp | +- test ----+- java pom.xml
The pom should contain this to start with
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.agical.experimental</groupId> <version>1.0-SNAPSHOT</version> <artifactId>SeleniumDemo</artifactId> <name>Selenium Demo</name> <packaging>war</packaging> <description>Demonstration on how to test a simple web app using Maven and Selenium</description> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-idea-plugin</artifactId> <version>2.2</version> <configuration> <downloadJavadocs>true</downloadJavadocs> <downloadSources>true</downloadSources> <jdkLevel>1.5</jdkLevel> <jdkName>1.5</jdkName> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.5</version> <scope>test</scope> </dependency> </dependencies> </project>
The things to note is that we want to use annotations and therefore we require to use at least Java 1.5, as stated in the maven-compiler-plugin. Next we want to create Intellij IDEA project files and if we state that we want both source and javadoc in the maven-idea-plugin, they will be downloaded as well. We will be able to read the javadoc as well as the source code for the third party libs from IDEA.
I plan to use Spring to simplify the controller so I add a requirement to the spring-webmvc and finally I want to be able to write test using JUnit so I specify that we want a dependency to junit.
Create the IDEA project files with
mvn idea:idea
and start IDEA so we can create the rest of the application.
Lets start with a simple test that should verify our controller in
src/test/java/com/agical/experimental/controller/RegistrationTest.java
package com.agical.experimental.controller; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import org.junit.Test; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import java.util.Map; public class RegistrationTest { @Test public void register() { Registration registration = new Registration(); String expectedName = "Thomas"; String expectedSurname = "Sundberg"; Model model = new ExtendedModelMap(); String expectedLandingPage = "registrationConfirmation"; String actualLandingPage = registration.registerGet(expectedName, expectedSurname, model); assertThat(actualLandingPage, is(expectedLandingPage)); assertTrue(model.containsAttribute("name")); assertTrue(model.containsAttribute("surname")); Map modelMap = model.asMap(); String actualName = (String) modelMap.get("name"); assertThat(actualName, is(expectedName)); String actualSurname = (String) modelMap.get("surname"); assertThat(actualSurname, is(expectedSurname)); } }
Nothing really exciting here. We define a small test that will verify that some values will be available in a model and that we refer to a landing page after the controller has been called.
This will of course not even compile since the production code isn't available. Lets create a simple controller in the production code and run the test until it works properly. Create
src/main/java/com/agical/experimental/controller/Registration.java
package com.agical.experimental.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/registration") public class Registration { @RequestMapping(method = RequestMethod.GET) public String registerGet(String name, String surname, Model model) { model.addAttribute("name", name); model.addAttribute("surname", surname); return "registrationConfirmation"; } }
A simple controller. This should be enough so we can compile and run a unit test. Lets do that.
mvn test
Ok, it compiled and the unit test passed. Let's package it as a web application now. We need to start with web.xml, create it as
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd"> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <display-name>Selenium Demo</display-name> <servlet> <servlet-name>Controller</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Controller</servlet-name> <url-pattern>/Controller/*</url-pattern> </servlet-mapping> </web-app>
Spring expect us to define a file named using this pattern: <servlet name in web.xml>-servlet.xml so lets define
src/main/webapp/WEB-INF/Controller-servlet.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd" default-autowire="byName"> <context:component-scan base-package="com.agical.experimental"/> <bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
We are missing a landing page and we are missing a start page. The start page isn't mandatory, but it is a nice way to test stuff either manually or automatically. Lets start with a landing page in
src/main/webapp/WEB-INF/jsp/registrationConfirmation.jsp
<%--@elvariable id="name" type="String"--%> <%--@elvariable id="surname" type="String"--%> <%@ page contentType="text/html;charset=ISO-8859-1" language="java" %> Welcome ${name} ${surname}!
The start page could be defined as below in
src/main/webapp/index.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <body> <form action="./Controller/registration" method="GET"> Name: <input id="name" type="text" name="name"/> <br> Surname: <input id="surname" type="text" name="surname"/> <br> <input id="submit" type="submit" value="Submit"/> </form> </body> </html>
We should now be able to build the web application and deploy it.
Lets build it using
mvn clean install
Deploy it somewhere, a running JBoss for example and try to access it from this url:
http://localhost:8080/SeleniumDemo-1.0-SNAPSHOT/
We were able to build a web application and deploy it manually. This is of course necessary if we ever should be able to automate the process. One tool that could help us with automatic deployment from Maven is Cargo. Lets add the stuff needed to deploy the application using Cargo to the pom. Cargo can deploy stuff in a lot of different applications servers but since this demonstration isn't really about deploying in different environments, we will use an embedded servlet container. Cargo has Jetty embedded so the only thing we need to add to deploy the application to Jetty is this snippet from the pom:
<plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <executions> <execution> <id>start-container</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <id>stop-container</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> <configuration> <container> <containerId>jetty6x</containerId> <type>embedded</type> </container> <wait>false</wait> </configuration> </plugin>
The important stuff here are:
We can't just add this section and hope it will work, we need to a repository where we can download the plugin definition.
<pluginRepositories> <pluginRepository> <id>mojo-snapshots</id> <name>codehause mojo snapshots</name> <layout>default</layout> <url>http://repository.codehaus.org</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories>
A working pom should look something like this now:
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.agical.experimental</groupId> <version>1.0-SNAPSHOT</version> <artifactId>SeleniumDemo</artifactId> <name>Selenium Demo</name> <packaging>war</packaging> <description>Demonstration on how to test a simple web app using Maven and Selenium</description> <pluginRepositories> <pluginRepository> <id>mojo-snapshots</id> <name>codehause mojo snapshots</name> <layout>default</layout> <url>http://repository.codehaus.org</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-idea-plugin</artifactId> <version>2.2</version> <configuration> <downloadJavadocs>true</downloadJavadocs> <downloadSources>true</downloadSources> <jdkLevel>1.5</jdkLevel> <jdkName>1.5</jdkName> </configuration> </plugin> <plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <executions> <execution> <id>start-container</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <id>stop-container</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> <configuration> <container> <containerId>jetty6x</containerId> <type>embedded</type> </container> <wait>false</wait> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.5</version> <scope>test</scope> </dependency> </dependencies> </project>
Perform
mvn integration-test
and note that a Jetty is started in port 8080. If you remove the <wait>false</wait> above, you should be able to access the web application. It will not work because we are still missing a small piece that is needed for Jetty.
With a web application that we can deploy from Maven, all we need is a good way to test it automatically.
First we need to add one more dependency in the pom:
<dependency> <groupId>org.seleniumhq.selenium.client-drivers</groupId> <artifactId>selenium-java-client-driver</artifactId> <version>1.0-beta-2</version> </dependency>
Re-create the IDEA project
mvn idea:idea
And finally add a integration test in
src/test/java/it/com/agical/experimental/controller/SeleniumTest.java
package it.com.agical.experimental.controller; import com.thoughtworks.selenium.DefaultSelenium; import com.thoughtworks.selenium.Selenium; import static junit.framework.Assert.assertTrue; import org.junit.After; import org.junit.Before; import org.junit.Test; public class SeleniumTest { private static final String DEFAULT_WAIT_PERIOD = "3000"; private Selenium selenium; @Before public void setUp() throws Exception { String host = "http://localhost:8080/"; String browser = "*firefox"; // String browser = "*iexplore"; // configure the selenium client selenium = new DefaultSelenium("localhost", 4444, browser, host); // launch the browser window selenium.start(); } @After public void tearDown() throws Exception { selenium.stop(); } @Test public void verifySimpleFormAndRespone() { selenium.open("SeleniumDemo-1.0-SNAPSHOT/"); selenium.waitForPageToLoad(DEFAULT_WAIT_PERIOD); String name = "Patricia"; String surname = "Persson"; selenium.type("name", name); selenium.type("surname", surname); selenium.click("submit"); selenium.waitForPageToLoad(DEFAULT_WAIT_PERIOD); assertTrue(selenium.isTextPresent(name)); assertTrue(selenium.isTextPresent(surname)); } }
There are two things to note here:
I will add some things to the pom so that we will be able to call an instance of Selenium on port 4444 later.
The other thing, the package 'it', is defined so we can differ the normal unit tests and the integration tests. There are basically two major ways to differ integration tests from unit tests.
I will use the latter approach in this example. The Surefire plug in can be configured like this:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <excludes> <exclude>**/it/**/*.java</exclude> </excludes> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>test</goal> </goals> <phase>integration-test</phase> <configuration> <excludes> <exclude>none</exclude> </excludes> <includes> <include>**/it/**/*.java</include> </includes> </configuration> </execution> </executions> </plugin>
Lets add stuff to the pom so we can run a Selenium remote control.
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>selenium-maven-plugin</artifactId> <version>1.0-rc-1</version> <executions> <execution> <id>start-selenium</id> <phase>pre-integration-test</phase> <goals> <goal>start-server</goal> </goals> <configuration> <background>true</background> </configuration> </execution> </executions> </plugin>
This will start a Selenium server in the background and it will in its turn be able to drive a browser from the integration test defined above.
We should be done by now, lets try.
mvn integration-test
It fails!
We are missing something. It turns out that we need to add a dependency to jstl in the pom. Add the dependency below:
<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>
A working pom should now look like this:
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.agical.experimental</groupId> <version>1.0-SNAPSHOT</version> <artifactId>SeleniumDemo</artifactId> <name>Selenium Demo</name> <packaging>war</packaging> <description>Demonstration on how to test a simple web app using Maven and Selenium</description> <repositories> <repository> <id>nexus</id> <name>Nexus Repository</name> <url>http://nexus.openqa.org/content/repositories/releases/</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>mojo-snapshots</id> <name>codehause mojo snapshots</name> <layout>default</layout> <url>http://repository.codehaus.org</url> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-idea-plugin</artifactId> <version>2.2</version> <configuration> <downloadJavadocs>true</downloadJavadocs> <downloadSources>true</downloadSources> <jdkLevel>1.5</jdkLevel> <jdkName>1.5</jdkName> </configuration> </plugin> <plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <executions> <execution> <id>start-container</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <id>stop-container</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> <configuration> <container> <containerId>jetty6x</containerId> <type>embedded</type> </container> <wait>false</wait> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>selenium-maven-plugin</artifactId> <version>1.0-rc-1</version> <executions> <execution> <id>start-selenium</id> <phase>pre-integration-test</phase> <goals> <goal>start-server</goal> </goals> <configuration> <background>true</background> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <excludes> <exclude>**/it/**/*.java</exclude> </excludes> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>test</goal> </goals> <phase>integration-test</phase> <configuration> <excludes> <exclude>none</exclude> </excludes> <includes> <include>**/it/**/*.java</include> </includes> </configuration> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.seleniumhq.selenium.client-drivers</groupId> <artifactId>selenium-java-client-driver</artifactId> <version>1.0-beta-2</version> <!--<scope>test</scope>--> </dependency> </dependencies> </project>
Run
mvn integration-test
again and the result should be that the application is built and tested from a browser.