Filed under: BDD, Cucumber, Test automation, — Tags: CXF, Cucumber-jvm, JSF, JUnit, Jersey, MVC, Model view controller, REST, RESTAssured, RESTFul, Selenium, Soap, Swing,, Swinggui, WebDriver, Wicket — Thomas Sundberg — 2012-11-01
Many modern applications are built as web applications. The benefits are obvious, you don't need to package your software in shrink-wrap and send it to your customers. Upgrading is easy, you have to upgrade the server you host the system on and that's it.
The first user interface I will add to the rental system will therefore be a web GUI. It will be the simplest possible solution and the goal is not to build a fancy web app. The goal is to show how Cucumber can control a tool like Selenium WebDriver to assert the behaviour of the web application.
This project will be divided in two different Maven modules and it will depend on the model I just developed. The modules are:
A parent project is added to connect the two modules.
The choice for this division is motivated in separating test and production code in a Maven build.
The first thing I will do is to setup the test module and have it to execute the Cucumber feature. I will use the same feature as for the model; my need for behaviour has not changed just because I am adding a GUI to the solution.
src/test/resources/se/waymark/rentit/Rent.feature
Feature: Rental cars should be possible to rent to gain revenue to the rental company. As an owner of a car rental company I want to make cars available for renting So I can make money Scenario: Find and rent a car Given there are 18 cars available for rental When I rent one Then there will only be 17 cars available for rental
The glue code to connect the feature above with Cucumber is also similar:
src/test/java/se/waymark/rentit/RunCukesIT.java
package se.waymark.rentit; import cucumber.api.junit.Cucumber; import org.junit.runner.RunWith; @RunWith(Cucumber.class) public class RunCukesIT { }
I have changed the name of the class to RunCukesIT
so the Maven fail-safe plugin will include it
without an explicit include in the plugin configuration.
The steps are also identical:
src/test/java/se/waymark/rentit/steps/RentStepdefs.java
package se.waymark.rentit.steps; import cucumber.api.java.en.Given; import cucumber.api.java.en.Then; import cucumber.api.java.en.When; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; public class RentStepdefs { private RentACarSupport rentACarSupport = new RentACarSupport(); @Given("^there are (\\d+) cars available for rental$") public void there_are_cars_available_for_rental(int availableCars) throws Throwable { rentACarSupport.createCars(availableCars); } @When("^I rent one$") public void rent_one_car() throws Throwable { rentACarSupport.rentACar(); } @Then("^there will only be (\\d+) cars available for rental$") public void there_will_be_less_cars_available_for_rental(int expectedAvailableCars) throws Throwable { int actualAvailableCars = rentACarSupport.getAvailableNumberOfCars(); assertThat(actualAvailableCars, is(expectedAvailableCars)); } }
The only differences in the test project are the help class and the Maven pom. The help class obviously need to use Selenium to connect to the running web application. The Maven pom will need to deploy the web application before it executes the test suite.
The new help class looks like this:
src/test/java/se/waymark/rentit/steps/RentACarSupport.java
package se.waymark.rentit.steps; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.firefox.FirefoxDriver; public class RentACarSupport { public void createCars(int availableCars) { WebDriver driver = new FirefoxDriver(); try { driver.get("http://localhost:8080/rentit/faces/create.xhtml"); WebElement numberOfCarsToCreate = driver.findElement(By.id("create:numberOfCars")); numberOfCarsToCreate.clear(); numberOfCarsToCreate.sendKeys("" + availableCars); WebElement createButton = driver.findElement(By.id("create:createButton")); createButton.click(); } finally { driver.close(); } } public void rentACar() { WebDriver driver = new FirefoxDriver(); try { driver.get("http://localhost:8080/rentit/faces/rent.xhtml"); WebElement rentButton = driver.findElement(By.id("rent:rentButton")); rentButton.click(); } finally { driver.close(); } } public int getAvailableNumberOfCars() { WebDriver driver = new FirefoxDriver(); try { driver.get("http://localhost:8080/rentit"); WebElement availableCars = driver.findElement(By.id("availableCars")); String availableCarsString = availableCars.getText(); return Integer.parseInt(availableCarsString); } finally { driver.close(); } } }
It consists of three methods. This is identical to the help class I wrote earlier. There is no reference to the domain model here. The web application must be deployed and running when these methods are executed. The state of the rental system will be held on the server.
The first thing I do in every method is to create a Firefox web driver so I can use Firefox to exercise the application. The second thing every method is doing is to connect to different web pages in the web application.
When the proper page has been located each method does different things. They locate different elements in the web
page and use them. As an example, the method createCars()
enters the number of cars to be created in a
form and then submits the form.
Obvious issues with these methods are that
This need to be fixed, but is out of the scope for this example.
The next thing that is different is the Maven pom. It has grown and some plugins has been added and configured. The pom now looks like this:
jsf-test/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.waymark</groupId> <artifactId>jsf-web-app</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>se.waymark</groupId> <artifactId>jsf-test</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.12</version> <executions> <execution> <id>integration-test</id> <phase>integration-test</phase> <goals> <goal>integration-test</goal> </goals> </execution> <execution> <id>verify</id> <phase>verify</phase> <goals> <goal>verify</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <version>1.2.2</version> <executions> <execution> <id>start-tomcat</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> </goals> </execution> <execution> <id>stop-tomcat</id> <phase>post-integration-test</phase> <goals> <goal>stop</goal> </goals> </execution> </executions> <configuration> <container> <containerId>tomcat7x</containerId> <zipUrlInstaller> <url>http://www.apache.org/dist/tomcat/tomcat-7/v7.0.32/bin/apache-tomcat-7.0.32.zip</url> </zipUrlInstaller> <output>${project.build.directory}/tomcat-logs/container.log</output> <append>false</append> <log>${project.build.directory}/tomcat-logs/cargo.log</log> </container> <configuration> <type>standalone</type> <home>${project.build.directory}/tomcat-home</home> <properties> <cargo.servlet.port>8080</cargo.servlet.port> <cargo.logging>high</cargo.logging> </properties> <deployables> <deployable> <groupId>se.waymark</groupId> <artifactId>jsf-main</artifactId> <type>war</type> </deployable> </deployables> </configuration> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>se.waymark</groupId> <artifactId>jsf-main</artifactId> <version>1.0-SNAPSHOT</version> <type>war</type> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-java</artifactId> <version>1.1.1</version> <scope>test</scope> </dependency> <dependency> <groupId>info.cukes</groupId> <artifactId>cucumber-junit</artifactId> <version>1.1.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>2.25.0</version> <scope>test</scope> </dependency> </dependencies> </project>
There are some things to notice with this pom. Lets go through them from the top.
maven-failsafe-plugin
cargo-maven2-plugin
zipUrlInstaller
is used to download Tomcat if it hasn't been downloaded yet. This means
that nobody has to install Tomcat manually for this project. This is a bit fragile. The URL to the
distribution zip changes when a new release of Tomcat is available. I suggest that you host the zip file at
a location that you have control over and specify that location instead.
These are the most important changes.
To be able to build the system, I need to implement the web application. This is a trivial JSF application and not really important but I include it for completeness. I will therefore skip through it fast. All files needed are included, but I will not go through the details. There are a lot of other people out there who are better sent to describe a JSF application than I am.
All files needed for this example are organised like this:
jsf-web-app |-- jsf-main | |-- pom.xml | `-- src | |-- main | | |-- java | | | `-- se | | | `-- waymark | | | `-- rentit | | | |-- controller | | | | `-- Controller.java | | | `-- view | | | |-- Available.java | | | |-- Create.java | | | `-- Rent.java | | `-- webapp | | |-- available.xhtml | | |-- create.xhtml | | |-- rent.xhtml | | `-- WEB-INF | | `-- web.xml | `-- test | `-- java | `-- se | `-- waymark | `-- rentit | `-- RentCarTest.java |-- jsf-test | |-- pom.xml | `-- src | `-- test | |-- java | | `-- se | | `-- waymark | | `-- rentit | | |-- RunCukesIT.java | | `-- steps | | |-- RentACarSupport.java | | `-- RentStepdefs.java | `-- resources | `-- se | `-- waymark | `-- rentit | `-- Rent.feature |-- pom.xml
All test files has already been presented so I will not do that again. The files needed for the web application, jsf-main, looks like this:
A parent pom is used to connect the two sub modules. It is defined as:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>se.waymark</groupId> <artifactId>jsf-web-app</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>jsf-main</module> <module>jsf-test</module> </modules> </project>
jsf-main/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.waymark</groupId> <artifactId>jsf-web-app</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>se.waymark</groupId> <artifactId>jsf-main</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <build> <finalName>rentit</finalName> </build> <dependencies> <dependency> <groupId>se.waymark.educational</groupId> <artifactId>model</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.sun.faces</groupId> <artifactId>jsf-api</artifactId> <version>2.1.7</version> </dependency> <dependency> <groupId>com.sun.faces</groupId> <artifactId>jsf-impl</artifactId> <version>2.1.7</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> </dependencies> </project>
The final name of the war will be 'rentit' so I don't have to bother with version number when I call the web application later.
src/main/java/se/waymark/rentit/controller/Controller.java
package se.waymark.rentit.controller; import se.waymark.rentit.model.dao.CarDAO; import se.waymark.rentit.model.dao.InMemoryCarDAO; import se.waymark.rentit.model.entiy.Car; public class Controller { private CarDAO carDAO = new InMemoryCarDAO(); public void createCar() { Car car = new Car(); carDAO.add(car); } public void rentCar() { Car car = carDAO.findAvailableCar(); car.rent(); } public int getNumberOfAvailableCars() { return carDAO.getNumberOfAvailableCars(); } }
I use a controller class to connect the view and model. Following Model View Controller, MVC, is always nice.
The view wants a Java bean to read properties from. There are three of them and they are defined as below.
src/main/java/se/waymark/rentit/view/Available.java
package se.waymark.rentit.view; import se.waymark.rentit.controller.Controller; import javax.faces.bean.ManagedBean; import javax.faces.bean.RequestScoped; @ManagedBean @RequestScoped public class Available { private Controller controller = new Controller(); public int getAvailableCars() { return controller.getNumberOfAvailableCars(); } }
src/main/java/se/waymark/rentit/view/Create.java
package se.waymark.rentit.view; import se.waymark.rentit.controller.Controller; import javax.faces.bean.ManagedBean; import javax.faces.bean.RequestScoped; @ManagedBean @RequestScoped public class Create { private Controller controller = new Controller(); private int numberOfCars; public String create() { for (int i = 0; i < numberOfCars; i++) { controller.createCar(); } return "available.xhtml"; } public void setNumberOfCars(int numberOfCars) { this.numberOfCars = numberOfCars; } public int getNumberOfCars() { return numberOfCars; } }
src/main/java/se/waymark/rentit/view/Rent.java
package se.waymark.rentit.view; import se.waymark.rentit.controller.Controller; import javax.faces.bean.ManagedBean; import javax.faces.bean.RequestScoped; @ManagedBean @RequestScoped public class Rent { private Controller controller = new Controller(); public String rent() { controller.rentCar(); return "available.xhtml"; } }
The user interface is built using xhtml files. The file names have to correspond to the Java beans above. They are defined as:
src/main/webapp/available.xhtml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Available cars</title> </head> <body> Available cars <table border="1"> <tr> <th>Car class</th> <th align="right">Available cars</th> </tr> <tr> <td>Compact</td> <td align="right" id="availableCars">#{available.availableCars}</td> </tr> </table> </body> </html>
src/main/webapp/create.xhtml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html"> <head> <title>Create cars</title> </head> <body> Create rental cars <h:form id="create"> <table> <tr> <td>Number of cars:</td> <td> <h:inputText required="true" id="numberOfCars" value="#{create.numberOfCars}"></h:inputText> <h:message for="numberOfCars"></h:message> </td> </tr> <tr> <td/> <td align="right"> <h:commandButton value="Create cars" id="createButton" action="#{create.create}"></h:commandButton> </td> </tr> </table> </h:form> </body> </html>
src/main/webapp/rent.xhtml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html"> <head> <title>Rent a car</title> </head> <body> Rent a car <h:form id="rent"> <h:commandButton value="Rent" id="rentButton" action="#{rent.rent}"></h:commandButton> </h:form> </body> </html>
A web xml has to be defined to connect the JSF components.
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <context-param> <param-name>javax.faces.STATE_SAVING_METHOD</param-name> <param-value>client</param-value> </context-param> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>/faces/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>faces/available.xhtml</welcome-file> </welcome-file-list> </web-app>
I wrote a simple test class to wire things together without using the web application.
src/test/java/se/waymark/rentit/RentCarTest.java
package se.waymark.rentit; import org.junit.Test; import se.waymark.rentit.view.Available; import se.waymark.rentit.view.Create; import se.waymark.rentit.view.Rent; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; public class RentCarTest { @Test public void shouldRentACar() { int initialNumberOfCars = 43; Create create = new Create(); create.setNumberOfCars(initialNumberOfCars); create.create(); int oneRentedCar = 1; int expected = initialNumberOfCars - oneRentedCar; Rent rent = new Rent(); rent.rent(); Available available = new Available(); int actual = available.getAvailableCars(); assertThat(actual, is(expected)); } }
A simple JSF application can be added on top of the model without changing the defined behaviour. This is Behaviour Driven Development. You define the behaviour you want. Implement it. Add a GUI if it is needed but don't change the behaviour if you don't have to. Next exercise is to use the same behaviour but another tool for the GUI. I will use Wicket instead of JSF.
Next - A Wicket web application