A Wicket web application

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.

The web application

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.

File organisation

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:

Parent pom

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>

Main application pom

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.

Application

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.

View

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>

Web application

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>

A test class

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));
    }

}

Conclusion

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.

Next - Building a Swing GUI



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
DevOps
Executable specification
Git
Gradle
Guice
J2EE
JUnit
Java
Javascript
Kubernetes
Linux
Load testing
Maven
Mockito
New developers
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
React
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS