Arquillian Tutorial: Writing Java EE 6 Integration Tests and more..
April 26th, 2012 by Micha KopsNow that the long awaited stable version of the Arquillian framework is released I wanted to demonstrate some interesting features of this framework that really eases writing and running of integration tests for Java EE 6 applications in many different ways.
In the following tutorial we are going to create some real-world examples using Enterprise JavaBeans, Contexts and Dependency Injection, the Java Persistence API and we’re finally running Drone/Selenium tests against a JEE Web Application that is using Java Server Faces.
Contents
Adding Arquillian to a Project
First of all we need the basic Arquillian framework so we’re adding the following snippet to our pom.xml
<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> </dependencies> </dependencyManagement>
Second we need a Java EE 6 container to run the stuff on .. Arquillian allows you to easily switch between different implementations using Maven profiles.
I somehow like GlassFish and so I am adding the following dependency to my pom.xml – if you’d like to run your test against another container, please refer to the container adapter documentation.
In addition we need JUnit .. no surprise here …
<dependencies> <dependency> <groupId>org.glassfish.main.extras</groupId> <artifactId>glassfish-embedded-all</artifactId> <version>3.1.2</version> <scope>provided</scope> </dependency> <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>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> </dependencies>
CDI Dependency Injection Testing
In our first scenario we want to test some good “old” dependency injection of an Enterprise Java Bean using CDI .. the first thing we need here is – of course – some CDI wired EJBs.
The stateless LocaleManager bean gets injected into the stateless TimeService bean
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"); } }
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) { return localeManager.getSpecialDateFormat().format(date); } }
This is our final integration test .. first we do some bootstrap using ShrinkWrap, then we get the TimeService injected and we finally assure that all session beans we’re correctly wired.
package com.hascode.tutorial.jee; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import java.util.Date; import javax.inject.Inject; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.junit.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(Arquillian.class) public class TimeServiceIT { private static final Date FIXED_DATE = new Date(1321009871); @Inject private TimeService timeService; @Deployment public static JavaArchive createArchiveAndDeploy() { return ShrinkWrap.create(JavaArchive.class) .addClasses(LocaleManager.class, TimeService.class) .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); } @Test public void testCreateTimestamp() { assertFalse("Sat, Feb 22, 2012".equals(timeService .getLocalizedTime(FIXED_DATE))); assertEquals("Fri, Jan 16, 1970", timeService.getLocalizedTime(FIXED_DATE)); } }
Running the test should produce the green bar that we developers love so much ;)
Java Persistence API Integration Tests
In the next example we’re going to test some EJBs making use of the Java Persistence API .. the first thing here is to define our persistence unit in src/main/resources/META-INF/persistence.xml.
Because we’re in a test environment here, we’re better off by adding another persistence.xml in src/test/resources/META-INF and use an HSQL database. I am using Toplink Essentials as persistence provider here because of the GlassFish-ized environment ;)
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <persistence-unit name="defaultPersistenceUnit" transaction-type="JTA"> <provider>oracle.toplink.essentials.PersistenceProvider</provider> <class>com.hascode.tutorial.jee.Book</class> <properties> <property name="javax.persistence.jdbc.driver" value="org.hsqldb.jdbcDriver"/> <property name="javax.persistence.jdbc.url" value="jdbc:hsqldb:mem:testdb"/> <property name="javax.persistence.jdbc.user" value="sa"/> <property name="javax.persistence.jdbc.password" value=""/> <property name="eclipselink.ddl-generation" value="create-tables"/> </properties> </persistence-unit> </persistence>
As you can see, we’ve already referenced an entity there so this is our Book entity .. we’re using Bean Validation to add some constraints to our fields, too …
package com.hascode.tutorial.jee; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.validation.constraints.NotNull; @Entity public class Book { @Id @GeneratedValue private Long id; @NotNull private String title; @NotNull private String author; public Long getId() { return id; } public void setId(final Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(final String title) { this.title = title; } public String getAuthor() { return author; } public void setAuthor(final String author) { this.author = author; } @Override public String toString() { return "Book [id=" + id + ", title=" + title + ", author=" + author + "]"; } }
In the next step we’re implementing a stateless session bean that holds a reference to the entity manager and allows us to save books and return a list of all available, persisted books
package com.hascode.tutorial.jee; import java.util.ArrayList; import java.util.List; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; @Stateless public class BookEJB { @PersistenceContext(unitName = "defaultPersistenceUnit") private EntityManager em; public Book saveBook(final Book book) { em.persist(book); return book; } public List<Book> findAllBooks() { final Query query = em .createQuery("SELECT b FROM Book b ORDER BY b.title ASC"); List<Book> entries = query.getResultList(); if (entries == null) { entries = new ArrayList<Book>(); } return entries; } public void deleteBook(final Book book) { book = em.merge(book); em.remove(book); } }
Finally the grand finale .. our integration test ..
package com.hascode.tutorial.jee; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import javax.inject.Inject; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.junit.Arquillian; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(Arquillian.class) public class BookServiceIT { @Inject private BookEJB bookEJB; @Deployment public static JavaArchive createArchiveAndDeploy() { return ShrinkWrap.create(JavaArchive.class) .addClasses(BookEJB.class, Book.class) .addAsResource("META-INF/persistence.xml") .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); } @Test public void testFetchBooks() { assertTrue(bookEJB.findAllBooks().isEmpty()); Book b1 = new Book(); b1.setAuthor("tim"); b1.setTitle("A fascinating book"); Book b2 = new Book(); b2.setAuthor("tom"); b2.setTitle("I R Coder"); Book b3 = new Book(); b3.setAuthor("maria"); b3.setTitle("Some book"); Book b4 = new Book(); b4.setAuthor("tim"); b4.setTitle("Another fascinating book"); bookEJB.saveBook(b1); bookEJB.saveBook(b2); bookEJB.saveBook(b3); bookEJB.saveBook(b4); assertEquals(4, bookEJB.findAllBooks().size()); bookEJB.deleteBook(b4); assertEquals(3, bookEJB.findAllBooks().size()); } }
Running the test you should see a shiny green bar again.
Testing a Java Server Faces Web App using Drone
First of all we need some additional dependencies in our pom.xml .. please add the following code
<dependencyManagement> <dependencies> [..] <dependency> <groupId>org.jboss.arquillian.extension</groupId> <artifactId>arquillian-drone-bom</artifactId> <version>1.0.0.Final</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<dependencies> [..] <dependency> <groupId>org.jboss.arquillian.junit</groupId> <artifactId>arquillian-junit-container</artifactId> <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>
Now that our pom.xml is stuffed we have to create a small Java Server Faces web application. Forgive me the code but we want to test against something “real” :)
In the first step add the following web.xml to src/main/webapp/WEB-INF .. we’re registering JSF and the welcome file here ..
<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>/books.xhtml</welcome-file> </welcome-file-list> </web-app>
We’re too lazy to create too much additional classes here so we’re reusing the code from the JPA example. Anyhow we need a managed bean and this is it:
package com.hascode.tutorial.jee; import java.util.List; import javax.faces.bean.ManagedBean; import javax.faces.bean.RequestScoped; import javax.inject.Inject; @ManagedBean(name = "bookControllerBean") @RequestScoped public class BookControllerBean { @Inject private BookEJB bookEJB; private Book book = new Book(); public String save() { bookEJB.saveBook(book); return "/books.xhtml"; } public List<Book> getBooks() { return bookEJB.findAllBooks(); // we're lazy here ;) } public Book getBook() { return book; } public void setBook(final Book book) { this.book = book; } }
In the next step we’re adding the following facelet named books.xhtml in src/main/webapp
<!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" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets"> <h:head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title> <ui:insert name="title">hasCode.com - Book Management App</ui:insert> </title> </h:head> <h:body> <p:panel> <h:panelGrid columns="2" cellpadding="10"> <h:panelGroup> <h1> <ui:insert name="heading">Java EE 6 Tutorial - Blog Application</ui:insert> </h1> </h:panelGroup> </h:panelGrid> <h2>#{bookControllerBean.books.size()} Books available:</h2> <ul> <ui:repeat value="#{bookControllerBean.books}" var="book"> <li> <h:outputText value="#{book.title} by #{book.author}"> </h:outputText> </li> </ui:repeat> </ul> <h2>Create new book:</h2> <h:messages> </h:messages> <h:form id="createBookForm"> <h:panelGrid columns="3" cellpadding="10"> <label>Title</label> <h:inputText label="Title" id="title" value="#{bookControllerBean.book.title}" required="true"> </h:inputText> <h:message for="title"> </h:message> <label>Author</label> <h:inputText label="Author" id="author" value="#{bookControllerBean.book.author}" required="true"> </h:inputText> <h:message for="author"> </h:message> <h:commandButton title="Save" value="Save" id="saveBook" action="#{bookControllerBean.save}"> </h:commandButton> </h:panelGrid> </h:form> </p:panel> </h:body> </html>
Finally our Drone test .. we get a hold of the Selenium instance using the @Drone annotation and we get the base URL using @ArquillianResource
package com.hascode.tutorial.jee; import static org.junit.Assert.assertTrue; 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.junit.Arquillian; 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.Test; import org.junit.runner.RunWith; import com.thoughtworks.selenium.DefaultSelenium; @RunWith(Arquillian.class) public class CreateBookSeleniumTest { private static final String WEBAPP_SRC = "src/main/webapp"; @Deployment(testable = false) public static WebArchive createDeployment() { return ShrinkWrap .create(WebArchive.class, "books.war") .addClasses(Book.class, BookEJB.class, BookControllerBean.class) .addAsResource("META-INF/persistence.xml") .addAsWebResource(new File(WEBAPP_SRC, "books.xhtml")) .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml") .addAsWebInfResource(EmptyAsset.INSTANCE, "faces-config.xml") .setWebXML(new File("src/main/webapp/WEB-INF/web.xml")); } @Drone DefaultSelenium browser; @ArquillianResource URL deploymentURL; @Test public void should_create_and_display_book() { // this will be http://localhost:8181/books/books.xhtml browser.open(deploymentURL + "books.xhtml"); // create a screenshot for the tutorial ;) // browser.captureScreenshot("/tmp/screen.png"); browser.type("id=createBookForm:title", "My book title"); browser.type("id=createBookForm:author", "The author"); browser.click("id=createBookForm:saveBook"); browser.waitForPageToLoad("20000"); assertTrue( "Book title present", browser.isElementPresent("xpath=//li[contains(text(), 'My book title by The author')]")); } }
The result of the test run should look like the following screencast of mine on YouTube (hopefully your environment is a bit faster ;)
If you need to set some breakpoints and do some step debugging that’s no problem either .. as to be seen in the following screencast on YouTube.
Tutorial Sources
I have put the source from this tutorial on my Bitbucket repository – download it there or check it out using Mercurial:
hg clone https://bitbucket.org/hascode/arquillian-tutorial
Troubleshooting
- “java.lang.RuntimeException: Could not invoke deployment method: public static org.jboss.shrinkwrap.api.spec.WebArchive // Caused by: java.lang.IllegalArgumentException: web.xml not found in classloader sun.misc.Launcher” – My web.xml resides in src/main/webapp – one reason for this is, that I am using an archetype that creates this structure for me .. to make ShrinkWrap add the web.xml from that location not on the direct classpath you need to pass in a java.io.File object and the web.xml should be added then.
Resources
Tutorial Sources
- 2018-06-01: Embedded YouTube video removed (GDPR/DSGVO).
Tags: arquillian, cdi, drone, ejb, glassfish, integration test, java ee, jboss, jee, jpa, jsf, selenium, tdd
April 26th, 2012 at 9:10 pm
Very nice write up!
To your troubleshooting: Using addAs|setXX(String) in ShrinkWrap will look for a Resource in your ClassLoader. web.xml is located in src/main/webapp, which is not normally not a part of your classpath.
note: Arquillian Drone 1.0.0.Final has been released as well :)
April 26th, 2012 at 9:20 pm
thanks for your remarks! article updated :)
May 4th, 2012 at 9:32 am
Good stuff! Thanks.
November 5th, 2012 at 12:55 pm
Hello , how are everyone.
Then, I’d like know how activate the file arquillian.xml to read in my arquillian project.
Anyone have any idea?
June 22nd, 2014 at 2:55 pm
Wonderful article and nice examples! Thanks!!!!
August 11th, 2015 at 11:52 am
Hello, thank you for the article. I tried to create an Integration for JPA(also using glassfish) test and gote some errors.
java.lang.IllegalArgumentException: ArquillianServletRunner not found. Could not determine ContextRoot from ProtocolMetadata, please contact DeployableContainer developer.
this is the first line of a log. Do you have a clue? Or maybe I am missing something?
August 11th, 2015 at 4:24 pm
Hi Mher,
is there any additional hint in your log files e.g. something mentioning a conflicting version of slf4j (known bug: http://java.net/jira/browse/GLASSFISH-16964)? Could you provide your simplified setup somewhere so that I could take a look?
September 16th, 2015 at 8:22 pm
I got the same error, java.lang.RuntimeException: Could not invoke deployment method: public static org.jboss.shrinkwrap.api.spec.WebArchive. Couldn’t understand how to troubleshoot, anyone please tel me in details.
Thanks
September 17th, 2015 at 9:03 am
Hi Harish,
could you please post the full exception cause? Is it “java.lang.IllegalArgumentException: web.xml not found in classloader sun.misc.Launcher” ?
September 25th, 2018 at 1:01 pm
I’m behind proxy How to set proxy with username and password?
September 26th, 2018 at 9:36 am
Hi Nick, good to hear it’s working now!