Here at Cambridge Technology Partners we are as serious about testing as we are about cutting-edge technologies like CDI. Last time we wrote about testing EJBs on embedded Glassfish and now we are back with something even more powerful, so keep on reading!
Background
Recently I was involved in a project based on JBoss Seam where we used Unitils for testing business logic and JPA. I really like this library, mainly because of the following aspects:
- Provides easy configuration and seamless integration of JPA (also a little bit of DI).
- Greatly simplifies management of the test data. All you need to do in order to seed your database with prepared data is providing a xml dataset (in a DBUnit flat xml format) and then add
@DataSet
annotation on the test class or method.
The only thing in Unitils which I'm not really comfortable with, is the fact that this library is not really designed for integration testing. The example which is clearly demonstrating it is an observer for Seam events. In this particular case we might need to leave unit testing world (mocks, spies and other test doubles) and develop real integration tests. The SeamTest module together with JBoss Embedded could help but it's really a tough task to make it running with Maven. On the other hand JBoss AS wasn't the target environment for us. Thankfully there is a new kid on the block from JBoss called Arquillian. In the next part of this post I will try to summarize my hands-on experience with this very promissing integration testing library. But first things first, let's look briefly at CDI events.
CDI Events
We are going to have JEE6 workshops for our customers and I was extremely happy when my colleagues asked me to play around with Arquillian and prepare some integration tests. I picked up a piece of logic responsible for logging market transactions based on CDI events. In brief it is a design technique which provides components interaction without any compilation-time dependencies. It's similar to the observer pattern but in case of CDI events, producers and observers are entirely decoupled from each other. If following example won't give you a clear explanation of the concept please refer to this well written blog post. Let's take a look at quite simplified code example.
import javax.ejb.Stateless;
import javax.enterprise.event.Event;
import javax.inject.Inject;
@Stateless
public class TradeService {
@Inject @Buy
private Event<ShareEvent> buyEvent;
public void buy(User user, Share share, Integer amount) {
user.addShares(share, amount);
ShareEvent shareEvent = new ShareEvent(share, user, amount);
buyEvent.fire(shareEvent);
}
...
}
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class TradeTransactionObserver implements Serializable {
...
@Inject
TradeTransactionDao tradeTransactionDao;
public void shareBought(@Observes @Buy ShareEvent event) {
TradeTransaction tradeTransaction = new TradeTransaction(event.getUser(), event.getShare(), event.getAmount(), TransactionType.BUY);
tradeTransactionDao.save(tradeTransaction);
}
...
}
To preserve the clear picture I'm not going to include Share
, TradeTransaction
, User
and ShareEvent
classes. What is worth to mention however is that the instance of ShareEvent
contains user, a share which he bought and amount. In the User
entity we store a map of shares together with amount using new @ElementCollection
annotation introduced in JPA 2.0. It allows to use entity classes as keys in the map.
@ElementCollection
@CollectionTable(name="USER_SHARES")
@Column(name="AMOUNT")
@MapKeyJoinColumn(name="SHARE_ID")
private Map<Share, Integer> shares = new HashMap<Share, Integer>();
Then in the TradeTransaction
entity we simply store this information and additionally date and TransactionType
. Complete code example could be downloaded from our google code page - see Resources section at the bottom of the post.
The very first test
We will use a following scenario for our test example (written in the BDD manner):
- given user choose a CTP share,
- when he buys it,
- then market transaction should be logged.
@Test
public void shouldLogTradeTransactionAfterBuyingShare() {
// given
User user = em.find(User.class, 1L);
Share share = shareDao.getByKey("CTP");
int amount = 1;
// when
tradeService.buy(user, share, amount);
// then
List<TradeTransaction> transactions = tradeTransactionDao.getTransactions(user);
assertThat(transactions).hasSize(1);
}
In the ideal world we could simply run this test in our favourite IDE or a build tool without writing a lot of plumbing code or dirty hacks to set up the environment like Glassfish, JBoss AS or Tomcat. And here's when Arquillian comes to play. The main goal of this project is to provide a convenient way for developers to run tests either in embedded or remote containers. It's still in alpha version but amount of already supported containers is really impressive. It opens the door to world of easy and pleasant to write integration tests. There are only two things required in order to make our tests "arquillian infected":
- Set
@RunWith(Arquillian.class)
annotation for you test class (or extendArquillian
base class if you are a TestNG guy). - Prepare a deployment package using ShrinkWrap API in a method marked by the
@Deployment
annotation.
@Deployment
public static Archive<?> createDeploymentPackage() {
return ShrinkWrap.create("test.jar", JavaArchive.class)
.addPackages(false, Share.class.getPackage(),
ShareEvent.class.getPackage(),
TradeTransactionDao.class.getPackage())
.addClass(TradeService.class)
.addManifestResource(new ByteArrayAsset("<beans/>".getBytes()), ArchivePaths.create("beans.xml"))
.addManifestResource("inmemory-test-persistence.xml", ArchivePaths.create("persistence.xml"));
}
Odds and ends
Until now I guess everything was rather easy to grasp. Unfortunately while playing with tests I encountered a few shortcomings but found the solutions which I hope make this post valuable for readers. Otherwise you could simply jump to the user guide and code examples, don't you?
JAR hell
The most time consuming issue were the dependencies conflicts better known as JAR hell. The target environment for the workshop application is Glassfish v3 so I used the embedded version for my integration tests. I decided to have my tests as integral part of the project and here problems began.
The main problem is that you cannot use javaee-api
because you will get exceptions while bootstrapping the container more or less similar to : java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/validation/constraints/Pattern$Flag (related thread on JBoss forum). I also recommend not to download separated jars for each project which you are using because you will get even more exceptions :)
Important remark here: if you are using JPA 2.0 with Criteria API and a hibernate-jpamodelgen
module for generating metamodel classes then you should also exclude org.hibernate.javax.persistence:hibernate-jpa-2.0-api
dependency to avoid yet another class conflict.
You have basically two options:
- Use
glassfish-embedded-all
jar since it already contains all needed APIs. - Create a separated project for integration testing and forget about everything what I mentioned in this section.
Preparing a database for testing
Next step is to create a data source for the Glassfish instance. But first we need to tell Arquillian to not delete the Glassfish server folder after each deployment / test execution (which is the default behaviour). All you need to do is to create a arquillian.xml
file and add following configuration:
<arquillian xmlns="http://jboss.com/arquillian"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:glassfish="urn:arq:org.jboss.arquillian.glassfish.embedded30">
<glassfish:container>
<glassfish:bindPort>9090</glassfish:bindPort>
<glassfish:instanceRoot>src/test/glassfish-embedded30</glassfish:instanceRoot>
<glassfish:autoDelete>false</glassfish:autoDelete>
</glassfish:container>
</arquillian>
Then we need to take a domain.xml
file from our normal Glassfish instance (i.e. from ${glassfish_home}/glassfish/domains/domain1/config
), remove all <applications>
and <system-applications>
nodes, add new data source and copy it to the src/test/glassfish-embedded-30/config
folder. We will use HSQL 1.8.0.7 version in our tests (2.0 version is causing some problems with DBUnit).
<domain log-root="${com.sun.aas.instanceRoot}/logs" application-root="${com.sun.aas.instanceRoot}/applications" version="22">
<system-applications />
<applications />
<resources>
...
<jdbc-connection-pool res-type="java.sql.Driver" description="In memory HSQLDB instance" name="arquilliandemo" driver-classname="org.hsqldb.jdbcDriver">
<property name="URL" value="jdbc:hsqldb:mem:arquilliandemomem" />
<property name="user" value="sa" />
<property name="password" value="" />
</jdbc-connection-pool>
<jdbc-resource pool-name="arquilliandemo" jndi-name="arquilliandemo-ds" />
</resources>
<servers>
<server name="server" config-ref="server-config">
...
<resource-ref ref="arquilliandemo-ds" />
</server>
</servers>
<configs>
...
</configs>
</domain>
The last file which you need to take from ${glassfish_home}/glassfish/domains/domain1/config
folder is server.policy
. And that's it! You have running Glassfish with HSQL database ready for some serious testing.
Data preparation
As I mentioned in the introductory section I really like Unitils and the way how you can seed the database with the test data. The only thing to do is to provide an XML file in flat dbunit format like this one:
<dataset>
<user id="1" firstname="John" lastname="Smith" username="username" password="password" />
<share id="1" key="CTP" price="18.00" />
</dataset>
and then put the @DataSet("test-data.xml")
annotation either on the test method or a class.
I was really missing this feature so I decided to implement it myself. Very cool way of adding such behaviour is by using a JUnit rule. This mechanism, similar to interceptors, has been available since 4.7 release. I choose to extend the TestWatchman
class since it provides methods to hook around test invocation. You can see the rule's logic based on the DBUnit flat xml data for seeding database in the example project. All you need to do is to create a public field in your test class and decorate it with @Rule
annotation. Here's the complete test class.
@RunWith(Arquillian.class)
public class TradeServiceTest {
@Deployment
public static Archive<?> createDeploymentPackage() {
return ShrinkWrap.create("test.jar", JavaArchive.class)
.addPackages(false, Share.class.getPackage(),
ShareEvent.class.getPackage(),
TradeTransactionDao.class.getPackage())
.addClass(TradeService.class)
.addManifestResource(new ByteArrayAsset("<beans />".getBytes()), ArchivePaths.create("beans.xml"))
.addManifestResource("inmemory-test-persistence.xml", ArchivePaths.create("persistence.xml"));
}
@Rule
public DataHandlingRule dataHandlingRule = new DataHandlingRule();
@PersistenceContext
EntityManager em;
@Inject
ShareDao shareDao;
@Inject
TradeTransactionDao tradeTransactionDao;
@Inject
TradeService tradeService;
@Test
@PrepareData("datasets/shares.xml")
public void shouldLogTradeTransactionAfterBuyingShare() {
// given
User user = em.find(User.class, 1L);
Share share = shareDao.getByKey("CTP");
int amount = 1;
// when
tradeService.buy(user, share, amount);
// then
List<TradeTransaction> transactions = tradeTransactionDao.getTransactions(user);
assertThat(transactions).hasSize(1);
}
}
I must admit that it's a JUnit specific solution, but you can always implement your own @BeforeTest
and @AfterTest
methods to achieve the same result in TestNG.
DBUnit gotchas
Using DBUnit's CLEAN_INSERT
strategy (or deleting table content after test execution by using DELETE_ALL
) could raise constraint violation exceptions. HSQL provides special SQL statement for this purpose and the sample project is invoking this statement just before DBUnit.
Final thoughts
All in all Arquillian is a really great integration testing tool with full of potential. It's just great that the JBoss guys are aiming to provide support for almost all widely used application servers and web containers. As you could see from the examples above it's not that hard to have tests for more sophisticated scenarios than you can find in the user guide. Keep your eyes on Arquillian - the roadmap is really promising.
In the upcoming second part I will dive into CDI contexts and demonstrate how to use Arquillian for testing contextual components.
If you are writing an application for the Java EE 6 stack while not using Arquillian is a serious mistake!
- Full code example with all aspects mentioned in the article (svn or zip)
- Fest-Assert - offers convenient assertions following fluent interface pattern (
assertThat()
). - JUnit Rules on InfoQ
- Unitils
8 comments:
Very Good, helpful Post. Could the code be reused? Also please note that I run into this issue as I was using remote container. https://issues.jboss.org/browse/ARQ-286
Hi Franck,
feel free to use code from the post [you can find link to google code in Resources section]. If you have any suggestions for improvements just let me know.
Thanks for pointing to the Jira issue.
My @Inject variables are ALWAYS null.
Any ideas?
Can you double check that your @RunWith(Arquillian.class) is in place? Looks like the @Inject is ignored. Otherwise CDI would fail the test before if a dependency cannot be fulfilled.
Yes, I just checked and it looks ok..
I have an open discussion here:
http://community.jboss.org/thread/164548
On the second page, I attached my code (7z).
Thanks,
Mark
Aha I see - looks like this drills down to fun with dependencies? If you can't use Maven:
- Can you use Ivy?
- Kind of a hack: Start with working Maven project and use the assembly plugin to fetch all required jars.
"You have basically two options:"
Of those two, which did you end up doing? I'm struggling through jar hell trying to get unit testing working on a NetBeans/GlassFish project, and it's got me going mad!
Hi Matthew,
quite a lot of things changed in Arquillian since my post. Please have a look at this guide, there is a description how to set up maven project for arquillian and glassfish embedded profile. If you will still face some problems don't hesitate to ask on the Arquillian forum.
Post a Comment