Having written about the basics of using Cucumber in a Java project in my last blog article, I now would like to demonstrate how to use a similar setup in a Java EE web project with Arquillian and the Cukespace library.
In the following tutorial, we’re going to write a full Java EE web application and add BDD-style tests to the project so that we’re able to test our business layer on the one hand and the user interface on the other hand using Arquillian Drone and Selenium.
Dependencies
The following dependency adds cukespace to your mavenized project’s pom.xml.
<dependency>
<groupId>com.github.cukespace</groupId>
<artifactId>cukespace-core</artifactId>
<version>1.5.10</version>
<scope>test</scope>
</dependency>
I have added the detailed excerpt with all dependencies for Arquillian, Drone, Selenium and JUnit at the end of the article for those who are interested.
Feature: Date Conversion
The first step is to write up our specification using the Gherkin syntax in the following text file named date_conversion.feature:
Feature: Date conversion
Scenario: Convert a specific date
Given a user named 'Tim'
When this user enters the date '2014-12-03 15:20:05' into the time conversion service
Then the service returns a conversion hint with the message 'hello Tim: the date converted is Wed, Dec 3, 2014'
Sample Application
Being lazy, I’ve decided to simply re-use a modified version of the application from my tutorial "Arquillian Tutorial: Writing Java EE 6 Integration Tests and more.." here as it contains a simple Java EE application also as some JSF Facelets for the graphical user interface.
If you’re not interested in the application rather than writing the tests, please feel free to #Example_1:_Testing_the_Business_Layer[skip directly to the tests].
Domain Model
Our user only has a name and is a simple POJO.
package com.hascode.tutorial.jee;
public class User {
private final String name;
public User(final String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Services
The following services are there to interact with the domain model:
LocaleManager
The locale manager is implemented as stateless session bean and returns a date format.
package com.hascode.tutorial.jee;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import javax.ejb.Stateless;
@Stateless
public class LocaleManager {
public DateFormat getSpecialDateFormat() {
return new SimpleDateFormat("EEE, MMM d, yyyy");
}
}
TimeService
The time service is implemented as an stateless session bean and returns a “personalized” date hint using the date format from the locale manager.
package com.hascode.tutorial.jee;
import java.util.Date;
import javax.ejb.Stateless;
import javax.inject.Inject;
@Stateless
public class TimeService {
@Inject
private LocaleManager localeManager;
public String getLocalizedTime(final Date date, final User user) {
return String.format("hello %s: the date converted is %s", user.getName(), localeManager.getSpecialDateFormat().format(date));
}
}
TimeConverterBean
This managed bean is there as a link to our JSF facelet – it’s request-scoped and get’s the time service injected.
package com.hascode.tutorial.jee;
import java.util.Date;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
@Named
@RequestScoped
public class TimeConverterBean {
private String userName;
private Date userDate;
private String dateConverted;
@Inject
private TimeService timeService;
public void doConvert() {
dateConverted = timeService.getLocalizedTime(userDate, new User(userName));
}
// getter, setter ommitted..
}
User Interface
The user interface is implement using Java Server Faces and a single facelet.
Facelet
The following facelet named conversion.xhtml is bound to the time conversion bean and prints out the converted date.
A user may enter his name and date here:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head>
<title><ui:insert name="title">hasCode.com - Time Conversion Sample</ui:insert></title>
</h:head>
<h:body>
<h:form id="timeConversion">
<h:panelGrid columns="1" cellpadding="10">
<h:panelGroup>
<label>Your Date</label>
<h:inputText label="Your Date" id="userDate" value="#{timeConverterBean.userDate}" required="true">
<f:convertDateTime pattern="yyyy-MM-dd HH:mm:ss"/>
</h:inputText>
</h:panelGroup>
<h:panelGroup>
<label>Your Name</label>
<h:inputText label="Your Name" id="userName" value="#{timeConverterBean.userName}" required="true" />
</h:panelGroup>
<h:panelGroup>
<label>Date converted</label>
<h:inputText id="dateConverted" value="#{timeConverterBean.dateConverted}" readonly="true"/>
</h:panelGroup>
<h:panelGroup>
<h:commandButton title="Convert" value="Convert" id="doConvert"
action="#{timeConverterBean.doConvert}">
</h:commandButton>
</h:panelGroup>
</h:panelGrid>
</h:form>
</h:body>
</html>
web.xml
The following declaration registers *.xhtml as the extension for facelets and sets the conversion.xhtml as welcome file.
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="3.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<display-name>hascode-arquillian</display-name>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>/conversion.xhtml</welcome-file>
</welcome-file-list>
</web-app>
Example 1: Testing the Business Layer
Now back to the more exciting stuff, writing tests ;)
Writing the Test
Our first test simply tests the collaboration of our service classes in a single, simple integration test.
We’re using Arquillian to run the tests on an embedded GlassFish container.
The CukeSpace JUnit runner allows us to easily embed our test into the JUnit and Arquillian life-cycle here, @Features is used to reference the plain-text file with the textual description of the desired behaviour of our feature, the rest of the test is mapped as we’re used to do with Cucumber-jvm using @Given, @When and @Then.
package it.feature;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Date;
import javax.inject.Inject;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.runner.RunWith;
import com.hascode.tutorial.jee.LocaleManager;
import com.hascode.tutorial.jee.TimeService;
import com.hascode.tutorial.jee.User;
import cucumber.api.Format;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import cucumber.runtime.arquillian.CukeSpace;
import cucumber.runtime.arquillian.api.Features;
@RunWith(CukeSpace.class)
@Features({ "src/test/resources/it/feature/date_conversion.feature" })
public class DateConversionFeatureIT {
@Deployment
public static JavaArchive createArchiveAndDeploy() {
return ShrinkWrap.create(JavaArchive.class).addClasses(LocaleManager.class, TimeService.class).addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
}
@Inject
TimeService timeService;
User user;
Date rawDate;
@Given("^a user named '(.+)'$")
public void create_user_with_name(final String name) throws Throwable {
user = new User(name);
}
@When("^this user enters the date '(.+)' into the time conversion service$")
public void this_user_enters_the_date_into_the_time_conversion_service(@Format("yyyy-MM-dd HH:mm:ss") final Date date) throws Throwable {
rawDate = date;
}
@Then("^the service returns a conversion hint with the message '(.*)'$")
public void the_service_returns_a_converted_date(final String dateConverted) throws Throwable {
assertThat(timeService.getLocalizedTime(rawDate, user), equalTo(dateConverted));
}
}
Running the Test
We’re now ready to run our test using Maven or the IDE of our choice here:
$ mvn test -Dtest=DateConversionFeatureIT 130 ↵
[..]
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running it.feature.DateConversionFeatureIT
[..]
[..]
21:10:22.804 INFO - SEC1011: Security Service(s) Started Successfully
21:10:22.924 INFO - WEB0169: Created HTTP listener [http-listener] on host/port [0.0.0.0:8181]
21:10:22.945 INFO - WEB0171: Created virtual server [server]
21:10:23.156 INFO - WEB0172: Virtual server [server] loaded default web module []
21:10:23.940 INFO - EJB5181:Portable JNDI names for EJB TimeService: [java:global/test/TimeService!com.hascode.tutorial.jee.TimeService, java:global/test/TimeService]
21:10:23.987 INFO - EJB5181:Portable JNDI names for EJB LocaleManager: [java:global/test/LocaleManager!com.hascode.tutorial.jee.LocaleManager, java:global/test/LocaleManager]
21:10:24.032 INFO - WELD-000900 SNAPSHOT
21:10:24.392 INFO - WEB0671: Loading application [test] at [/test]
21:10:24.434 INFO - test was successfully deployed in 1,903 milliseconds.
21:10:24.812 INFO - Running src/test/resources/it/feature/date_conversion.feature
Feature: Date conversion
Scenario: Convert a specific date # src/test/resources/it/feature/date_conversion.feature:3
Given a user named 'Tim' # DateConversionFeatureIT.create_user_with_name(String)
When this user enters the date '2014-12-03 15:20:05' into the time conversion service # DateConversionFeatureIT.this_user_enters_the_date_into_the_time_conversion_service(Date)
Then the service returns a conversion hint with the message 'hello Tim: the date converted is Wed, Dec 3, 2014' # DateConversionFeatureIT.the_service_returns_a_converted_date(String)
1 Scenarios (1 passed)
3 Steps (3 passed)
0m0.078s
PlainTextActionReporterSUCCESSNo monitoring data to report.
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.988 sec
[..]
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.960s
[INFO] Finished at: Mon Jan 05 21:10:26 CET 2015
[INFO] Final Memory: 10M/205M
[INFO] ------------------------------------------------------------------------
Example 2: GUI Testing using Drone
In our second example, testing is more complex because we’re testing the user interface using Selenium and the Arquillian Drone project.
Writing the Test
Again we’re using a similar setup as in the first test but we’re adding testable=false to the deployment to run it outside the container.
@Drone injects an instance of selenium and the dynamic URL of our application in the embedded application container is obtained using @ArquillianResource.
In the following steps we’re using selenium to enter data into the web browser started when running the test.
package it.feature;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import java.io.File;
import java.net.URL;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.runner.RunWith;
import com.thoughtworks.selenium.DefaultSelenium;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import cucumber.runtime.arquillian.CukeSpace;
import cucumber.runtime.arquillian.api.Features;
@RunWith(CukeSpace.class)
@Features({ "src/test/resources/it/feature/date_conversion.feature" })
public class DateConversionFeature2IT {
@Deployment(testable = false)
public static WebArchive createDeployment() {
return ShrinkWrap.create(WebArchive.class, "sample.war").addPackages(true, "com.hascode").addAsWebResource(new File("src/main/webapp", "conversion.xhtml"))
.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml").setWebXML(new File("src/main/webapp/WEB-INF/web.xml"));
}
@Drone
DefaultSelenium browser;
@ArquillianResource
URL deploymentURL;
String userName;
@Given("^a user named '(.+)'$")
public void create_user_with_name(final String name) throws Throwable {
userName = name;
}
@When("^this user enters the date '(.+)' into the time conversion service$")
public void this_user_enters_the_date_into_the_time_conversion_service(final String dateString) throws Throwable {
browser.open(deploymentURL + "conversion.xhtml");
browser.type("id=timeConversion:userDate", dateString);
browser.type("id=timeConversion:userName", userName);
browser.click("id=timeConversion:doConvert");
browser.waitForPageToLoad("20000");
}
@Then("^the service returns a conversion hint with the message '(.*)'$")
public void the_service_returns_a_converted_date(final String dateConverted) throws Throwable {
assertThat(browser.getValue("timeConversion:dateConverted"), equalTo(dateConverted));
}
}
Running the Test
We’re now ready to run our test using Maven or the IDE of our choice here:
$ mvn test -Dtest=DateConversionFeature2IT 1 ↵
[..]
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running it.feature.DateConversionFeature2IT
21:12:49.081 INFO - RemoteWebDriver instances should connect to: http://127.0.0.1:14444/wd/hub
21:12:49.084 INFO - Version Jetty/5.1.x
21:12:49.086 INFO - Started HttpContext[/selenium-server,/selenium-server]
21:12:49.097 INFO - Started org.openqa.jetty.jetty.servlet.ServletHandler@6b04acb2
21:12:49.097 INFO - Started HttpContext[/wd,/wd]
21:12:49.098 INFO - Started HttpContext[/selenium-server/driver,/selenium-server/driver]
21:12:49.098 INFO - Started HttpContext[/,/]
21:12:49.100 INFO - Started SocketListener on 0.0.0.0:14444
21:12:49.101 INFO - Started org.openqa.jetty.jetty.Server@589b028e
[..]
21:12:50.465 INFO - EJB5181:Portable JNDI names for EJB TimeService: [java:global/sample/TimeService!com.hascode.tutorial.jee.TimeService, java:global/sample/TimeService]
21:12:50.498 INFO - EJB5181:Portable JNDI names for EJB LocaleManager: [java:global/sample/LocaleManager, java:global/sample/LocaleManager!com.hascode.tutorial.jee.LocaleManager]
21:12:50.537 INFO - WELD-000900 SNAPSHOT
21:12:50.989 INFO - Initializing Mojarra 2.1.6 (SNAPSHOT 20111206) for context '/sample'
21:12:51.705 INFO - WEB0671: Loading application [sample] at [/sample]
21:12:51.747 INFO - sample was successfully deployed in 2,586 milliseconds.
21:12:51.788 INFO - Checking Resource aliases
21:12:51.791 INFO - Command request: getNewBrowserSession[*firefox, http://localhost:8080, ] on session null
21:12:51.793 INFO - creating new remote session
21:12:51.808 INFO - Allocated session 15474ecbf00540288984b6eb16abe8af for http://localhost:8080, launching...
jar:file:/home/private/soma/.m2/repository/org/seleniumhq/selenium/selenium-server/2.29.0/selenium-server-2.29.0.jar!/customProfileDirCUSTFFCHROME
21:12:51.830 INFO - Preparing Firefox profile...
21:12:52.883 INFO - Launching Firefox...
21:12:55.218 INFO - Got result: OK,15474ecbf00540288984b6eb16abe8af on session 15474ecbf00540288984b6eb16abe8af
21:12:55.227 INFO - Command request: setSpeed[0, ] on session 15474ecbf00540288984b6eb16abe8af
21:12:55.228 INFO - Got result: OK on session 15474ecbf00540288984b6eb16abe8af
21:12:55.231 INFO - Command request: setTimeout[60000, ] on session 15474ecbf00540288984b6eb16abe8af
21:12:55.258 INFO - Got result: OK on session 15474ecbf00540288984b6eb16abe8af
21:12:55.427 INFO - Running src/test/resources/it/feature/date_conversion.feature
Feature: Date conversion
21:12:55.502 INFO - Command request: open[http://localhost:8181/sample/conversion.xhtml, ] on session 15474ecbf00540288984b6eb16abe8af
21:12:55.871 INFO - Got result: OK on session 15474ecbf00540288984b6eb16abe8af
21:12:55.872 INFO - Command request: type[id=timeConversion:userDate, 2014-12-03 15:20:05] on session 15474ecbf00540288984b6eb16abe8af
21:12:55.881 INFO - Got result: OK on session 15474ecbf00540288984b6eb16abe8af
21:12:55.883 INFO - Command request: type[id=timeConversion:userName, Tim] on session 15474ecbf00540288984b6eb16abe8af
21:12:55.889 INFO - Got result: OK on session 15474ecbf00540288984b6eb16abe8af
21:12:55.890 INFO - Command request: click[id=timeConversion:doConvert, ] on session 15474ecbf00540288984b6eb16abe8af
21:12:55.899 INFO - Got result: OK on session 15474ecbf00540288984b6eb16abe8af
21:12:55.900 INFO - Command request: waitForPageToLoad[20000, ] on session 15474ecbf00540288984b6eb16abe8af
21:12:56.161 INFO - Got result: OK on session 15474ecbf00540288984b6eb16abe8af
21:12:56.163 INFO - Command request: getValue[timeConversion:dateConverted, ] on session 15474ecbf00540288984b6eb16abe8af
21:12:56.180 INFO - Got result: OK,hello Tim: the date converted is Wed, Dec 3, 2014 on session 15474ecbf00540288984b6eb16abe8af
Scenario: Convert a specific date # src/test/resources/it/feature/date_conversion.feature:3
Given a user named 'Tim' # DateConversionFeature2IT.create_user_with_name(String)
When this user enters the date '2014-12-03 15:20:05' into the time conversion service # DateConversionFeature2IT.this_user_enters_the_date_into_the_time_conversion_service(String)
Then the service returns a conversion hint with the message 'hello Tim: the date converted is Wed, Dec 3, 2014' # DateConversionFeature2IT.the_service_returns_a_converted_date(String)
1 Scenarios (1 passed)
3 Steps (3 passed)
0m0.749s
[..]
21:12:56.254 INFO - Killing Firefox...
[..]
Results :
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.179s
[INFO] Finished at: Mon Jan 05 21:12:57 CET 2015
[INFO] Final Memory: 10M/205M
[INFO] ------------------------------------------------------------------------
Or simply run the tests using your favourite IDE with a decent JUnit runner.
Dependencies (detailed)
Here is a more complete list of dependencies. For the full list, please feel free to have a look at my git repository here.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.arquillian</groupId>
<artifactId>arquillian-bom</artifactId>
<version>1.0.0.Final</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-bom</artifactId>
<version>1.1.1.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
[..]
<dependency>
<groupId>org.jboss.arquillian.container</groupId>
<artifactId>arquillian-glassfish-embedded-3.1</artifactId>
<version>1.0.0.CR3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-container</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-container</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.cukespace</groupId>
<artifactId>cukespace-core</artifactId>
<version>1.5.10</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-impl</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-selenium</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.extension</groupId>
<artifactId>arquillian-drone-selenium-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-server</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.mortbay.jetty</groupId>
<artifactId>servlet-api-2.5</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
Directory Structure
My project’s directory structure looks like this one:
.
├── pom.xml
├── README.md
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── hascode
│ │ └── tutorial
│ │ └── jee
│ │ ├── LocaleManager.java
│ │ ├── TimeConverterBean.java
│ │ ├── TimeService.java
│ │ └── User.java
│ ├── resources
│ └── webapp
│ ├── conversion.xhtml
│ └── WEB-INF
│ ├── beans.xml
│ └── web.xml
└── test
├── java
│ └── it
│ └── feature
│ ├── DateConversionFeature2IT.java
│ └── DateConversionFeatureIT.java
└── resources
└── it
└── feature
└── date_conversion.feature
Tutorial Sources
Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/cukespace-cucumber-arquillian-tutorial.git
Resources
Other BDD Articles of mine
The following articles of mine are covering different aspects and frameworks for Behaviour Driven Development:
Article Updates
-
2017-04-06: Links to other BDD articles of mine added.