SeamFramework.orgCommunity Documentation
Most Seam applications will need at least two kinds of automated tests: unit tests, which test a particular Seam component in isolation, and scripted integration tests which exercise all Java layers of the application (that is, everything except the view pages).
Both kinds of tests are very easy to write.
All Seam components are POJOs. This is a great place to start if you want easy unit testing. And since Seam emphasises the use of bijection for inter-component interactions and access to contextual objects, it's very easy to test a Seam component outside of its normal runtime environment.
Consider the following Seam Component which creates a statement of account for a customer:
@Stateless
@Scope(EVENT)
@Name("statementOfAccount")
public class StatementOfAccount {
@In(create=true) EntityManager entityManager
private double statementTotal;
@In
private Customer customer;
@Create
public void create() {
List<Invoice> invoices = entityManager
.createQuery("select invoice from Invoice invoice where invoice.customer = :customer")
.setParameter("customer", customer)
.getResultList();
statementTotal = calculateTotal(invoices);
}
public double calculateTotal(List<Invoice> invoices) {
double total = 0.0;
for (Invoice invoice: invoices)
{
double += invoice.getTotal();
}
return total;
}
// getter and setter for statementTotal
}
We could write a unit test for the calculateTotal method (which tests the business logic of the component) as follows:
public class StatementOfAccountTest {
@Test
public testCalculateTotal {
List<Invoice> invoices = generateTestInvoices(); // A test data generator
double statementTotal = new StatementOfAccount().calculateTotal(invoices);
assert statementTotal = 123.45;
}
}
You'll notice we aren't testing retrieving data from or persisting data to the database; nor are we testing any functionality provided by Seam. We are just testing the logic of our POJOs. Seam components don't usually depend directly upon container infrastructure, so most unit testing are as easy as that!
However, if you want to test the entire application, read on.
Integration testing is slightly more difficult. In this case, we can't eliminate the container infrastructure; indeed, that is part of what is being tested! At the same time, we don't want to be forced to deploy our application to an application server to run the automated tests. We need to be able to reproduce just enough of the container infrastructure inside our testing environment to be able to exercise the whole application, without hurting performance too much.
The approach taken by Seam is to let you write tests that exercise your components while running inside a pruned down container environment (Seam, together with the JBoss Embedded container; see Section 30.6.1, “Installing Embedded JBoss” for configuration details)
public class RegisterTest extends SeamTest
{
@Test
public void testRegisterComponent() throws Exception
{
new ComponentTest() {
protected void testComponents() throws Exception
{
setValue("#{user.username}", "1ovthafew");
setValue("#{user.name}", "Gavin King");
setValue("#{user.password}", "secret");
assert invokeMethod("#{register.register}").equals("success");
assert getValue("#{user.username}").equals("1ovthafew");
assert getValue("#{user.name}").equals("Gavin King");
assert getValue("#{user.password}").equals("secret");
}
}.run();
}
...
}
Occasionally, we need to be able to replace the implementation of some Seam component that depends upon resources which are not available in the integration test environment. For example, suppose we have some Seam component which is a facade to some payment processing system:
@Name("paymentProcessor")
public class PaymentProcessor {
public boolean processPayment(Payment payment) { .... }
}
For integration tests, we can mock out this component as follows:
@Name("paymentProcessor")
@Install(precedence=MOCK)
public class MockPaymentProcessor extends PaymentProcessor {
public boolean processPayment(Payment payment) {
return true;
}
}
Since the MOCK
precedence is higher than the default
precedence of application components, Seam will install the mock
implementation whenever it is in the classpath. When deployed into
production, the mock implementation is absent, so the real component
will be installed.
An even harder problem is emulating user interactions. A third problem is where to put our assertions. Some test frameworks let us test the whole application by reproducing user interactions with the web browser. These frameworks have their place, but they are not appropriate for use at development time.
SeamTest
lets you write scripted tests,
in a simulated JSF environment. The role of a scripted test is to reproduce
the interaction between the view and the Seam components. In other words, you
get to pretend you are the JSF implementation!
This approach tests everything except the view.
Let's consider a JSP view for the component we unit tested above:
<html>
<head>
<title>Register New User</title>
</head>
<body>
<f:view>
<h:form>
<table border="0">
<tr>
<td>Username</td>
<td><h:inputText value="#{user.username}"/></td>
</tr>
<tr>
<td>Real Name</td>
<td><h:inputText value="#{user.name}"/></td>
</tr>
<tr>
<td>Password</td>
<td><h:inputSecret value="#{user.password}"/></td>
</tr>
</table>
<h:messages/>
<h:commandButton type="submit" value="Register" action="#{register.register}"/>
</h:form>
</f:view>
</body>
</html>
We want to test the registration functionality of our application (the stuff that happens when the user clicks the Register button). We'll reproduce the JSF request lifecycle in an automated TestNG test:
public class RegisterTest extends SeamTest
{
@Test
public void testRegister() throws Exception
{
new FacesRequest() {
@Override
protected void processValidations() throws Exception
{
validateValue("#{user.username}", "1ovthafew");
validateValue("#{user.name}", "Gavin King");
validateValue("#{user.password}", "secret");
assert !isValidationFailure();
}
@Override
protected void updateModelValues() throws Exception
{
setValue("#{user.username}", "1ovthafew");
setValue("#{user.name}", "Gavin King");
setValue("#{user.password}", "secret");
}
@Override
protected void invokeApplication()
{
assert invokeMethod("#{register.register}").equals("success");
}
@Override
protected void renderResponse()
{
assert getValue("#{user.username}").equals("1ovthafew");
assert getValue("#{user.name}").equals("Gavin King");
assert getValue("#{user.password}").equals("secret");
}
}.run();
}
...
}
Notice that we've extended SeamTest
, which provides a
Seam environment for our components, and written our test script as an
anonymous class that extends SeamTest.FacesRequest
,
which provides an emulated JSF request lifecycle. (There is also a
SeamTest.NonFacesRequest
for testing GET requests.)
We've written our code in methods which are named for the various JSF
phases, to emulate the calls that JSF would make to our components. Then
we've thrown in various assertions.
You'll find plenty of integration tests for the Seam example applications which demonstrate more complex cases. There are instructions for running these tests using Ant, or using the TestNG plugin for eclipse:
If you used seam-gen to create your project you are ready to start writing tests. Otherwise you'll need to setup the testing environment in your favorite build tool (e.g. ant, maven, eclipse).
First, lets look at the dependencies you need at a minimum:
Table 37.1.
Group Id | Artifact Id | Location in Seam |
---|---|---|
org.jboss.seam.embedded
|
hibernate-all
|
lib/test/hibernate-all.jar
|
org.jboss.seam.embedded
|
jboss-embedded-all
|
lib/test/jboss-embedded-all.jar
|
org.jboss.seam.embedded
|
thirdparty-all
|
lib/test/thirdparty-all.jar
|
org.jboss.seam.embedded
|
jboss-embedded-api
|
lib/jboss-embedded-api.jar
|
org.jboss.seam
|
jboss-seam
|
lib/jboss-seam.jar
|
org.jboss.el
|
jboss-el
|
lib/jboss-el.jar
|
javax.faces
|
jsf-api
|
lib/jsf-api.jar
|
javax.el
|
el-api
|
lib/el-api.jar
|
javax.activation
|
javax.activation
|
lib/activation.jar
|
It's very important you don't put the compile time JBoss AS
dependencies from lib/
(e.g.
jboss-system.jar
) on the classpath, these
will cause Embedded JBoss to not boot. So, just add the
dependencies (e.g. Drools, jBPM)you need as you go.
You also need to include the bootstrap/
directory on the classpath; bootstrap/
contains
the configuration for Embedded JBoss.
And, of course you need to put your built project and tests onto
the classpath as well as jar for your test framework. Don't forget
to put all the correct configuration files for JPA and Seam onto
the classpath as well.Seam asks Embedded JBoss to deploy any
resource (jar or directory) which has
seam.properties
in it's root. Therefore, if you
don't assemble a directory structure that resembles a deployable
archive containing your built project, you must put a
seam.properties
in each resource.
By default, a generated project will use the
java:/DefaultDS
(a built in HSQL datasource in
Embedded JBoss) for testing. If you want to use another datasource
place the foo-ds.xml
into
bootstrap/deploy
directory.
Seam provides TestNG support out of the box, but you can also use another test framework, such as JUnit, if you want.
You'll need to provide an implementation of
AbstractSeamTest
which does the following:
Calls super.begin()
before every test
method.
Calls super.end()
after every test
method.
Calls super.setupClass()
to setup
integration test environment. This should be called before
any test methods are called.
Calls super.cleanupClass()
to clean up
the integration test environment.
Calls super.startSeam()
to start Seam at
the start of integration testing.
Calls super.stopSeam()
to cleanly shut
down Seam at the end of integration testing.
If you want to insert or clean data in your database before each
test you can use Seam's integration with DBUnit. To do this, extend
DBUnitSeamTest
rather than SeamTest
.
You have to provide a dataset for DBUnit.
DBUnitSeamTest
assumes the flat format is used, so make sure that
your dataset is in this format.
<dataset>
<ARTIST
id="1"
dtype="Band"
name="Pink Floyd" />
<DISC
id="1"
name="Dark Side of the Moon"
artist_id="1" />
</dataset>
In your test class, configure your dataset with overriding
prepareDBUnitOperations()
:
protected void prepareDBUnitOperations() {
beforeTestOperations.add(
new DataSetOperation("my/datasets/BaseData.xml")
);
}
DataSetOperation
defaults to DatabaseOperation.CLEAN_INSERT
if no other operation is specified as a constructor argument. The
above example cleans all tables defined BaseData.xml
,
then inserts all rows declared in BaseData.xml
before each @Test
method is invoked.
If you require extra cleanup after a test method executes, add
operations to afterTestOperations
list.
You need to tell DBUnit about the datasource you are using by
setting a TestNG test parameter named datasourceJndiName
:
<parameter name="datasourceJndiName" value="java:/seamdiscsDatasource"/>
DBUnitSeamTest has support for MySQL and HSQL - you need to tell it which database is being used, otherwise it defaults to HSQL:
<parameter name="database" value="MYSQL" />
It also allows you to insert binary data into the test data set (n.b. this is untested on Windows). You need to tell it where to locate these resources on your classpath:
<parameter name="binaryDir" value="images/" />
You do not have to configure any of these parameters if you use HSQL and have
no binary imports. However, unless you specify datasourceJndiName
in your test configuration, you will have to call setDatabaseJndiName()
before your test runs. If you are not using HSQL or MySQL, you need to override some
methods. See the Javadoc of DBUnitSeamTest
for more details.
It's very easy to integration test your Seam Mail:
public class MailTest extends SeamTest {
@Test
public void testSimpleMessage() throws Exception {
new FacesRequest() {
@Override
protected void updateModelValues() throws Exception {
setValue("#{person.firstname}", "Pete");
setValue("#{person.lastname}", "Muir");
setValue("#{person.address}", "[email protected]");
}
@Override
protected void invokeApplication() throws Exception {
MimeMessage renderedMessage = getRenderedMailMessage("/simple.xhtml");
assert renderedMessage.getAllRecipients().length == 1;
InternetAddress to = (InternetAddress) renderedMessage.getAllRecipients()[0];
assert to.getAddress().equals("[email protected]");
}
}.run();
}
}
We create a new FacesRequest
as normal. Inside
the invokeApplication hook we render the message using
getRenderedMailMessage(viewId);
, passing the
viewId of the message to render. The method returns the rendered
message on which you can do your tests. You can of course also use
any of the standard JSF lifecycle methods.
There is no support for rendering standard JSF components so you can't test the content body of the mail message easily.