A JSF 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 - Building the model

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.

These are the most important changes.

The web application

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.

File organisation

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:

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>jsf-web-app</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>jsf-main</module>
        <module>jsf-test</module>
    </modules>
</project>

Main application pom

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.

Controller

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.

View

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>

Web application

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>

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

Conclusion

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



(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