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 as 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; n.b. JBoss Embedded requires JDK 1.5 and does not work with JDK 1.6).
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 33.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/test/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.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. 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.
If you need 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 need to provide a dataset for DBUnit:
<dataset> <ARTIST id="1" dtype="Band" name="Pink Floyd" /> <DISC id="1" name="Dark Side of the Moon" artist_id="1" /> </dataset>
and tell Seam about it by 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"/>
Warning! This feature is still under development.
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}", "test@example.com"); } @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("test@example.com"); } }.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.