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
Previous - A JSF web application
A wicket application is yet another web application. I divide the project in two parts as earlier. The only large difference is the support class that will connect to the system under test. It has been adapted for another web application.
The feature is still the same:
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 identical:
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 { }
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.core.Is.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 first interesting difference is in the help class. It now deals with Wicket rather than JSF:
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/create"); WebElement numberOfCarsToCreate = driver.findElement(By.id("numberOfCars")); numberOfCarsToCreate.clear(); numberOfCarsToCreate.sendKeys("" + availableCars); WebElement createButton = driver.findElement(By.id("createButton")); createButton.click(); } finally { driver.close(); } } public void rentACar() { WebDriver driver = new FirefoxDriver(); try { driver.get("http://localhost:8080/rentit/rent"); WebElement rentButton = driver.findElement(By.id("rentButton")); rentButton.click(); } finally { driver.close(); } } public int getAvailableNumberOfCars() { WebDriver driver = new FirefoxDriver(); try { driver.get("http://localhost:8080/rentit/available"); WebElement availableCars = driver.findElement(By.id("availableCars")); String availableCarsString = availableCars.getText(); return Integer.parseInt(availableCarsString); } finally { driver.close(); } } }
The Wicket help class suffers of the same problems as the JSF support class. These shortcomings need to be addressed, but not today.
This was the interesting part from a testing point of view.
To execute this, we need a Maven pom that defines the project
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.waymark</groupId> <artifactId>wicket-web-app</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>se.waymark</groupId> <artifactId>wicket-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>wicket-main</artifactId> <type>war</type> </deployable> </deployables> </configuration> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>se.waymark</groupId> <artifactId>wicket-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>
It is very similar to the JSF version. It uses the Maven fail-safe plugin to execute the test. The Cargo plugin is used to launch Tomcat and deploy the web application on it.
To be able to build the system, I need to implement the web application. This is a trivial Wicket application and not important but here 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 Wicket application than I am.
The files needed for this example are organised like this:
wicket-web-app |-- pom.xml |-- wicket-main | |-- pom.xml | `-- src | |-- main | | |-- java | | | `-- se | | | `-- waymark | | | `-- rentit | | | |-- Application.java | | | `-- view | | | |-- Available.java | | | |-- Create.java | | | `-- Rent.java | | |-- resources | | | `-- se | | | `-- waymark | | | `-- rentit | | | `-- view | | | |-- Available.html | | | |-- Create.html | | | `-- Rent.html | | `-- webapp | | `-- WEB-INF | | `-- web.xml | `-- test | `-- java | `-- se | `-- waymark | `-- rentit | `-- RentCarTest.java `-- wicket-test |-- pom.xml `-- src `-- test |-- java | `-- se | `-- waymark | `-- rentit | |-- RunCukesIT.java | `-- steps | |-- RentACarSupport.java | `-- RentStepdefs.java `-- resources `-- se `-- waymark `-- rentit `-- Rent.feature
All test files has already been presented so I will not do that again. The files needed for the web application, wicket-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>wicket-web-app</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>wicket-main</module> <module>wicket-test</module> </modules> </project>
wicket-main/pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>se.waymark</groupId> <artifactId>wicket-web-app</artifactId> <version>1.0-SNAPSHOT</version> </parent> <groupId>se.waymark</groupId> <artifactId>wicket-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>org.apache.wicket</groupId> <artifactId>wicket-core</artifactId> <version>1.5.7</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-servlet-api</artifactId> <version>7.0.29</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.
Wicket wants a class to be the starting point for a web application. This root class may look like this:
src/main/java/se/waymark/rentit/Application.java
package se.waymark.rentit; import org.apache.wicket.protocol.http.WebApplication; import se.waymark.rentit.model.dao.CarDAO; import se.waymark.rentit.model.dao.InMemoryCarDAO; import se.waymark.rentit.model.entiy.Car; import se.waymark.rentit.view.Available; import se.waymark.rentit.view.Create; import se.waymark.rentit.view.Rent; public class Application extends WebApplication { private CarDAO carDAO = new InMemoryCarDAO(); @Override protected void init() { mountPage("create", Create.class); mountPage("available", Available.class); mountPage("rent", Rent.class); } @Override public Class<Available> getHomePage() { return Available.class; } 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(); } }
It extends WebApplication
and overrides some methods.
The view in Wicket is created using a Java class and an html file. They have to be named the same way and has to be
located in the same package. Following the Maven way, I locate the Java classes in /java
source code
and the html files in the same package in /resources
.
Finding the number of available cars is implemented as:
src/main/java/se/waymark/rentit/view/Available.java
package se.waymark.rentit.view; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.basic.Label; import se.waymark.rentit.Application; public class Available extends WebPage { private Application application; public Available() { application = (Application) getApplication(); String availableCars = "" + getAvailableCars(); Label message = new Label("availableCars", availableCars); add(message); } public int getAvailableCars() { return application.getNumberOfAvailableCars(); } }
src/main/resources/se/waymark/rentit/view/Available.html
<!DOCTYPE html> <html xmlns:wicket="http://wicket.apache.org"> 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"> <span wicket:id="availableCars">Available cars</span> </td> </tr> </table> </html>
Creating cars is implemented as:
src/main/java/se/waymark/rentit/view/Create.java
package se.waymark.rentit.view; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.FormComponent; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.util.value.ValueMap; import se.waymark.rentit.Application; public class Create extends WebPage { private Application application; private int numberOfCars; public Create() { CreateCarsForm createCarsForm = new CreateCarsForm("createCarsForm"); add(createCarsForm); application = (Application) getApplication(); } public void setNumberOfCars(int initialNumberOfCars) { numberOfCars = initialNumberOfCars; } public void create() { for (int i = 0; i < numberOfCars; i++) { application.createCar(); } } private class CreateCarsForm extends Form<ValueMap> { public CreateCarsForm(String id) { super(id, new CompoundPropertyModel<ValueMap>(new ValueMap())); FormComponent<Integer> textField = new TextField<Integer>("numberOfCarsField"); textField.setType(String.class); add(textField); } @Override public final void onSubmit() { ValueMap values = getModelObject(); String addedCars = (String) values.get("numberOfCarsField"); numberOfCars = Integer.parseInt(addedCars); create(); setResponsePage(Available.class); } } }
src/main/resources/se/waymark/rentit/view/Create.html
<!DOCTYPE html> <html xmlns:wicket="http://wicket.apache.org"> <form wicket:id="createCarsForm"> <table> <tr> <td>Number of cars:</td> <td> <input id="numberOfCars" wicket:id="numberOfCarsField"/> </td> </tr> <tr> <td/> <td align="right"> <input type="submit" value="Create cars" id="createButton"/> </td> </tr> </table> </form> </html>
Renting a car is implemented as:
src/main/java/se/waymark/rentit/view/Rent.java
package se.waymark.rentit.view; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.model.CompoundPropertyModel; import org.apache.wicket.util.value.ValueMap; import se.waymark.rentit.Application; public class Rent extends WebPage { private Application application; public Rent() { RentCarForm rentCarForm = new RentCarForm("rentCarForm"); add(rentCarForm); application = (Application) getApplication(); } public void rent() { application.rentCar(); } private class RentCarForm extends Form<ValueMap> { public RentCarForm(String id) { super(id, new CompoundPropertyModel<ValueMap>(new ValueMap())); } @Override public final void onSubmit() { rent(); setResponsePage(Available.class); } } }
src/main/resources/se/waymark/rentit/view/Rent.html
<!DOCTYPE html> <html xmlns:wicket="http://wicket.apache.org"> <form wicket:id="rentCarForm"> <input type="submit" value="Rent" id="rentButton"/> </form> </html>
A web xml has to be defined to connect the Wicket components.
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <filter> <filter-name>wicket.hello</filter-name> <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class> <init-param> <param-name>applicationClassName</param-name> <param-value>se.waymark.rentit.Application</param-value> </init-param> </filter> <filter-mapping> <filter-name>wicket.hello</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </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.apache.wicket.util.tester.WicketTester; 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 { private Application application = new Application(); private WicketTester wicketTester = new WicketTester(application); @Test public void shouldRentACar() { int initialNumberOfCars = 43; Create create = wicketTester.startPage(Create.class); create.setNumberOfCars(initialNumberOfCars); create.create(); int oneRentedCar = 1; int expected = initialNumberOfCars - oneRentedCar; Rent rent = wicketTester.startPage(Rent.class); rent.rent(); Available available = wicketTester.startPage(Available.class); int actual = available.getAvailableCars(); assertThat(actual, is(expected)); } }
A simple Wicket 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 a Swing application instead of a web application this time.