In-Container Testing with JUnit

Developer: J2EE and OpenSource

by Julien Dubois

Learn how in-container testing with JUnit is superior to mock objects for integration testing, how to apply that technique using Oracle JDeveloper.

Today, unit testing is a very popular technique for ensuring code quality. Thanks to the JUnit framework, it has become quite easy to write unit tests for simple Java applications. However, where real-world enterprise applications are concerned, the typical JUnit testXXX()method will not be as helpful because those applications require that objects run inside a container.

In this article, I will describe the application of in-container testing in order to make JUnit tests access objects running inside a J2EE container. The example used here is a Struts-based Web application, which is fairly common among enterprise applications, but the techniques discussed are relevant to any J2EE project.

Introducing JUnit

The best way to introduce JUnit is to show off some code, so let's start with a simple example, a JavaBean called "Message".

  package jdubois.otn.cactus.domain; import java.io.Serializable; import java.util.Calendar; import java.util.Date; public class Message implements Serializable, Comparable { private Date postDate; private String user; private String comment; public Message(String user) { this.postDate = Calendar.getInstance().getTime(); this.user = user; } public void setUser(String user) { this.user = user; } public String getUser() { return user; } public void setComment(String comment) { this.comment = comment; } public String getComment() { return comment; } public Date getPostDate() { return postDate; } public int compareTo(Object o) { return this.postDate.compareTo(((Message) o).getPostDate()); } } 

Here is a simple JUnit test, which checks the JavaBean constructor:

  package jdubois.otn.cactus.test; import junit.framework.*; import jdubois.otn.cactus.domain.Message; public class MessageTester extends TestCase { public MessageTester(String sTestName) { super(sTestName); } public void testMessage() { Message message = new Message("user"); assertEquals("user", message.getUser()); assertNotNull(message.getPostDate()); } public static void main(String[] args) { junit.textui.TestRunner.run(MessageTester.class); } } 

All there is to do with JUnit is create a testXXX() method, run some code inside the method, and then check whether everything is all right with the various assertXXX(..) methods. This is quite simple to write but not really useful for testing today's J2EE applications. In fact, things get a lot more complicated as you enter the real enterprise applications space; the main problem is that, in J2EE land, most objects need to run inside a container.

Let's write a simple Struts action:

  package jdubois.otn.cactus.web; import java.util.TreeSet; import org.apache.struts.action.*; import javax.servlet.http.*; public class LoginAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { LoginForm frm = (LoginForm) form; request.getSession().setAttribute("user", frm.getName()); if (request.getSession().getServletContext().getAttribute("messages") == null) { request.getSession().getServletContext().setAttribute("messages", new TreeSet()); } return mapping.findForward("success"); } } 

The execute(...) method is really difficult to test. Creating an ActionForm is OK; but how is it possible, outside a container, to generate an ActionMapping, an HttpServletRequest, and an HttpServletResponse? The same problem can occur with most objects in J2EE; they are not made to run outside a container, and it will be difficult to test them with a normal, container-free JUnit test.

There are two popular ways of dealing with this problem: mock objects and in-container testing.

Mock objects mock real-world J2EE objects. A framework such as MockObjects ( http://www.mockobjects.com) provides special implementations of some of the J2EE APIs, in order to use those objects from inside a JUnit test with no container running. For example, it provides a "MockHttpServletRequest" object.

In-container testing makes JUnit tests run inside a J2EE container. To achieve this goal, the J2EE application being tested sometimes needs to be slightly modified, depending on the tool used (Cactus and HttpUnit, in this document).

This article will focus on that second option, as it is easier to set up and perform a "real life" test. In-container testing requires less coding and adds a layer of integration testing that is most useful in practice. From this author's point of view, the current mock objects framework also lacks maturity and documentation. The downside of in-container testing is that tests will run more slowly, as the application needs to be deployed onto a J2EE server—which should not be a problem with OC4J, as it starts up rather quickly.

Overview

Sample application. For the purpose of this article, a sample application called "Discussion Board" will be created. It is a very simple discussion board where users can log in and post messages.

For simplicity's sake, this example does not use a persistent store; in other words, no database is being used. This is a simplification that will not occur in the real world. Most projects use an object-relational mapping tool—such as CMP EJBs or TopLink—and as such, the focus of the testing is not necessarily the persistence layer.

Tools used. Running in-container tests requires some help from a few open source projects.

  • Cactus ( http://jakarta.apache.org/cactus/index.html ) is an extension to JUnit that eases considerably the process of running in-container tests. Cactus provides a framework for testing JSPs, Servlets, taglibs, and servlet filters and has comprehensive documentation.
  • StrutsTestCase ( http://strutstestcase.sourceforge.net ) is an extension of Cactus that is specifically designed for testing Struts code. In a nutshell, StrutsTestCase is an extension of Cactus, which is an extension of JUnit. By using these three projects together, it is possible to easily perform unit tests on a large scope of objects, from simple JavaBeans to Servlets to Struts actions.
  • HttpUnit ( http://httpunit.sourceforge.net ) takes a rather different approach. It emulates a Web browser, and when used with JUnit it allows efficient testing of the Web pages rendered by the application. Therefore, it is more of a "black box" testing tool.

Installing JUnit, Cactus, StrutsTestCase, and HttpUnit

JUnit. Installing JUnit is a simple matter of launching JDeveloper, clicking on "help -> check for updates", and selecting JUnit from the available extensions. In order to use JUnit with Ant, copy the junit.jar file from <JDEVELOPER_INSTALL_DIR>\junit3.8.1 to your ant "/lib" directory.

Cactus. Cactus is an Apache Jakarta project, which can be found at http://jakarta.apache.org/cactus/index.html . The download page is located at http://jakarta.apache.org/cactus/downloads.html . For this article, the "jakarta-cactus-13-1.6.1.zip" file was used. Unzip this file into a directory—c:\java\cactus, for example.

StrutsTestCase. StrutsTestCase is a SourceForge project, which can be found at http://strutstestcase.sourceforge.net . For this article, the "strutstest210-1.1_2.3.zip" file was used. Unzip this file into a directory—c:\java\strutstest, for example.

HttpUnit. HttpUnit is another SourceForge project, which lives at http://httpunit.sourceforge.net . For this article, version 1.5.4 was used. Unzip "httpunit-1.5.4.zip" into a directory next to StrutsTestCase—c:\java\httpunit, for example.

Configuring JDeveloper

  • Launch JDeveloper and create a new application workspace.
  • Right-click on the project, select "project properties", and go to "profiles -> development -> libraries".
  • Add the Cactus libraries:
    • Select "new", and name the new library "Cactus".
    • In the classpath, add all the libraries contained in <CACTUS_INSTALL_DIR>\lib, excepted junit-3.8.1.jar and servletapi-2.3.jar (those will be added to the classpath later).
    • The Cactus documentation can also be added; it is stored in <CACTUS_INSTALL_DIR>\doc\api\framework-13.
  • Add the StrutsTestCase libraries:
    • Select "new", and name the new library "StrutsTest".
    • In the classpath, add the "<STRUTSTESTCASE_INSTALL_DIR>\strutstest-2.1.0.jar" file.
    • The documentation can also be installed; it is kept in "<STRUTSTESTCASE_INSTALL_DIR>\docs\api".
  • Add the HttpUnit libraries:
    • Select "new", and add a new library "HttpUnit".
    • In the classpath, add nekohtml.jar and xercesImpl.jar from the "<HTTPUNIT_INSTALL_DIR>\jars" directory. This is the easiest way to get started with HttpUnit. If this does not suit you, you can use JTidy (which is also provided in the /jars directory), a stricter HTML parser.
    • If you need JavaScript support, simply add js.jar library (which is also provided in the same directory). This library works surprisingly well—at least for small scripts that alter forms—so it is possible to test a Web site even if you do some JavaScript magic on the client side.
    • Add the HttpUnit library, located in "<HTTPUNIT_INSTALL_DIR>\lib".
    • The documentation is located in "<HTTPUNIT_INSTALL_DIR>\doc\api".

Important note: Cactus and HttpUnit do not play well together, as they both use the NekoHtml library. As they cover the same kind of functionality, it is expected that you won't try to use both of them at the same time. However, if you wish to do so from within JDeveloper, you will have to use NekoHtml with HttpUnit (and not JTidy), and you will have to build a single JDeveloper library containing all the Cactus and HttpUnit jars. If you do run into a java.lang.reflect.InvocationTargetException, it stems from the above problem. The other libraries needed for the project—such as JUnit runtime, JSP runtime, Struts runtime, and Servlet runtime—will be automatically added by JDeveloper during the creation of the sample application.

Creating the Sample Application

The sample application will be a Struts-based discussion board. Create a new application in JDeveloper, and right-click on the project: select new -> Web tier -> Struts -> Struts Contruller Page Flow.

The page flow in Figure 1 will be used:

Figure 1: Sample Page Flow for a Struts-based Discussion Board

  <?xml version = '1.0' encoding = 'windows-1252'?> <!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd"> <struts-config> <form-beans> <form-bean name="loginForm" type="jdubois.otn.cactus.web.LoginForm"/> <form-bean name="messageForm" type="jdubois.otn.cactus.web.MessageForm"/> </form-beans> <action-mappings> <action name="loginForm" path="/Login" type="jdubois.otn.cactus.web.LoginAction" unknown="true" input="/login.jsp"> <forward name="success" path="/messages.jsp" redirect="true"/> </action> <action name="messageForm" path="/AddMessage" type="jdubois.otn.cactus.web.AddMessageAction" unknown="false"> <forward name="success" path="/messages.jsp"/> <forward name="login" path="/login.jsp"/> </action> </action-mappings> <message-resources parameter="jdubois.otn.cactus.web.ApplicationResources"/> </struts-config> 

In order to generate the page flow diagram, copy/paste the above code in the struts-config.xml file, go to the "diagram" tab, and refresh it (right-click -> diagram -> refresh diagram from struts-config).

Getting Started with a Simple JUnit Test

  • 1. To get started, create the Message JavaBean as described in the introductory section.
  • 2. Right-click on the "Application Sources", and add a new class. Copy/paste the code from the introduction.
  • 3. Now, let's also create the simple JUnit test from the introduction: Right-click on the "Application Sources" -> General -> Unit Tests (JUnit) -> Test Case.
  • 4. In the wizard, select the newly created Message JavaBean as the class to test.
  • 5. The resulting file is ready for testing; it just needs a few testXXX() methods, such as the one described in the introduction. Copy and paste the testMessage() method:
      public void testMessage() { Message message = new Message("user"); assertEquals("user", message.getUser()); assertNotNull(message.getPostDate()); } 
  • To ease the development from within JDeveloper, a main(...) method can be added:
      public static void main(String[] args) { junit.textui.TestRunner.run(MessageTester.class); } 

    When this method is added, JDeveloper recognizes the JUnit test, and right-clicking on the class brings a "Run -> As a JUnit Test Case" menu.

  • 7. Using this menu, the JUnit Test will execute inside the JDeveloper consule, at the bottom of the screen.

What's interesting is, of course, when a test fails, not when it is successful. Let's make the previous test fail by adding in the middle of the method:

  assertTrue(false); 

This code will always fail, and JUnit will diagnose a failure. When this kind of problem occurs (and we do hope they will occur—after all, it's why we're creating tests), there are two useful helpers:

  • JUnit gives a nice stack trace, on which it is possible to click in order to find where the code failed.
  • The JDeveloper debugger works with JUnit, so it is possible to run the test again with the debugger on, and some break points set.

Creating a Test Suite

Test suites are groups of tests. In JDeveloper, they are especially convenient:

  • They can be created graphically, using a wizard.
  • They launch the JUnit graphical interface.

Here is how to get started with a simple test suite:

  • Right-click on the "Application Sources", and add a new "Test Suite" from the "Unit Tests" group.
  • When asked by the wizard, call your Test Suite "AllTests", and place the previously created Test Case in the Test Suite.
  • When the wizard is finished, run the Test Suite (see Figure 2):

Figure 2: Running a Test Suite

A quick glance at the generated source code for the Test Suite shows that it is very easy to add new Test Cases to the Test Suite:

  suite.addTestSuite(jdubois.otn.cactus.test.MessageTester.class); 

Cactifying the Sample Application

Now that a simple JUnit test has been realized, it's time to move on to the real thing: J2EE application testing.

Before running any Cactus in-container J2EE tests, the application needs to be "cactified." This is just a matter of modifying web.xml and adding a properties file. This "cactification" can be done manually or by using an Ant script.

Manual cactification. Cactus uses a special servlet in order to run unit tests on the container from a standard JUnit test runner.

A new servlet, and its associated mapping, needs to be added to the web.xml file:

  <servlet> <servlet-name>ServletRedirector</servlet-name> <servlet-class>org.apache.cactus.server.ServletTestRedirector</servlet-class> </servlet> <servlet-mapping> <servlet-name>ServletRedirector</servlet-name> <url-pattern>/ServletRedirector</url-pattern> </servlet-mapping> 

The cactus test runner needs a properties file, called cactus.properties, in the classpath. Right-click on the "Application Sources" tree, add a new file, and create "cactus.properties" at the root of the source tree. This file needs three properties:

  cactus.contextURL = http://localhost:8988/user-Project-context-root cactus.servletRedirectorName=ServletRedirector cactus.enableLogging=true 

The "user-Project-context-root" is the deployment context of the current application. If you are unsure of the name of this context, right-click on the "login.jsp" file and select "run": in the newly opened browser, the URL to the current application will be visible.

The <war/> and <cactifywar/> Ant tasks. The <war/> task is a standard Ant task for creating war .

  <!-- create the war file --> <war destfile="./discussion-board.war" webxml="public_html/WEB-INF/web.xml"> <fileset dir="public_html"> <exclude name="WEB-INF/web.xml"/> </fileset> <lib dir="${jdev.home}/jakarta-struts/lib/*"> </lib> <classes dir="classes"/> </war> 

More documentation and examples are available on Ant's Web site: http://ant.apache.org .

The Cactus distribution provides a number of additional tasks for Ant. The most important one is the <cactifywar/> task.

The <cactifywar/> task analyzes a normal war file, and transforms it into a cactified war. It automates the manual steps we performed in the previous section. The only minor drawback is that it is a bit slow to run, as it keeps unzipping/cactifying/zipping the war file. It also allows keeping a standard, non-cactified war file for deployment. Important to note is that the ServletRedirector servlet should not be bundled in a production war, as it can be a potential security breach.

From the Cactus Web site ( http://jakarta.apache.org/cactus/integration/ant/index.html ), the Cactus Ant integration is fairly simple:

  <!-- Define the Cactus tasks --> <!-- If you generate the build.xml file with JDeveloper, all the Cactus files should already be in the classpath, so there is no need to redefine it --> <taskdef resource="cactus.tasks"> <classpath refid="classpath"/> </taskdef> <!-- Cactify the web-app archive --> <cactifywar srcfile="./discussion-board.war" destfile="="./cactified-discussion-board.war"> <servletredirector/> <lib file="<STRUTSTESTCASE_INSTALL_DIR>/strutstest-2.1.0.jar"/> </cactifywar> 

If you have problems using Ant and Cactus, the first thing to do is to copy the junit.jar file into the ant "/lib" directory, as explained in the "Installing JUnit" section of this article. Cactus provides other Ant tasks to automate the testing process; they are covered at the end of this article, in the " Next Steps " section.

Creating and Testing the LoginAction

Creating the action. The LoginAction uses a FormBean and two JSPs (login.jsp and messages.jsp). Let's create those three objects.

An easy way to create those files is to open the Struts Page Flow:

  • Double-click on the LoginAction to create it.
  • Right-click on the action and "go to FormBean" to create the FormBean.
  • Double-click on the JSPs to create them.

LoginAction.java

  package jdubois.otn.cactus.web; import java.util.TreeSet; import javax.servlet.http.*; import javax.servlet.ServletContext; import org.apache.struts.action.*; public class LoginAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { LoginForm frm = (LoginForm) form; request.getSession().setAttribute("user", frm.getName()); ServletContext context = request.getSession().getServletContext(); if (context.getAttribute("messages") == null) { context.setAttribute("messages", new TreeSet()); } return mapping.findForward("success"); } } 

LoginForm.java

  package jdubois.otn.cactus.web; import org.apache.struts.action.*; import javax.servlet.http.HttpServletRequest; public class LoginForm extends ActionForm { private String name; public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) { ActionErrors errors = new ActionErrors(); if (this.name == null || this.name.equals("")) { errors.add("error", new ActionError("user.name.required")); } return errors; } public void setName(String name) { this.name = name; } public String getName() { return name; } } 

Note that the ActionError uses the "user.name.required" key, specified in Struts' ApplicationResources.properties file:

  user.name.required=A user name is required 

login.jsp

  <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%> <%@ page contentType="text/html;charset=windows-1252"%> <html> <head> <meta http-equiv="Content-Type" CONTENT="text/html;charset=utf-8"> <title>Welcome</title> </head> <body> <div align="center"> <html:form action="/Login"> <h2>Welcome to our online discussion board</h2> <html:errors/> <p>Please enter your name : <html:text property="name" size="20"/> <html:submit value="Login"/> </p> </html:form> </div> </body> </html> 

messages.jsp

  <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%> <%@ page contentType="text/html;charset=windows-1252"%> <html> <head> <meta http-equiv="Content-Type" CONTENT="text/html;charset=utf-8"> <title>Discussion</title> </head> <body> <div align="center"> <table border="1"> <tr> <th>User</th> <th>Message</th> </tr> <logic:iterate id="message" name="messages"> <tr> <td> <bean:write name="message" property="user"/> </td> <td> <bean:write name="message" property="comment"/> </td> </tr> </logic:iterate> </table> </div> </body> </html> 

Right-click on "login.jsp", and select "run". A browser Window will open, and it will be possible to use the LoginAction (see Figure 3):

Figure 3: Discussion Board Login Window

Testing the action with StrutsTestCase. Start your testing by asking yourself the question, "what do we want to test?"

  • That a successful login creates a "user" object in Session scope
  • That a successful login forwards the user to the messages.jsp page
  • That a failed login does not create a "user" object in Session scope
  • That a failed login redirects the user to the login page
  • That a failed login creates a "user.name.required" ActionError

It would be helpful to get these questions answered by the future users of the application, to help define application flow.

Now let's test the LoginAction. Select the "Application Sources", and create a new class.

LoginActionTester.java

  package jdubois.otn.cactus.test; import servletunit.struts.CactusStrutsTestCase; public class LoginActionTester extends CactusStrutsTestCase { public LoginActionTester(String testName) { super(testName); } public void testSuccessfulLogin() { addRequestParameter("name","Julien"); setRequestPathInfo("/Login"); actionPerform(); verifyForward("success"); verifyForwardPath("/messages.jsp"); assertEquals("Julien", getSession().getAttribute("user")); verifyNoActionErrors(); } public void testFailedLogin() { setRequestPathInfo("/Login"); actionPerform(); verifyInputForward(); verifyForwardPath("/login.jsp"); verifyActionErrors(new String[] {"user.name.required"}); assertNull(getSession().getAttribute("user")); } public static void main(String[] args) { junit.textui.TestRunner.run(LoginActionTester.class); } } 

This test case is a CactusStrutsTestCase, which is an object provided by StrutsTestCase (which was installed earlier). It uses both JUnit and Cactus to test Struts actions.

In this case, the testSuccessfulLogin() method:

  • Performs the action
  • Verifies that the action forwarded to "success", which is in fact the "messages.jsp" page
  • Verifies that the "user" object was created
  • Verifies that no ActionError was produced

And the testFailedLogin() method:

  • Performs the action
  • Verifies that the action forwarded to the input page, which is "login.jsp"
  • Verifies that the "user.name.required" ActionError was declared
  • Verifies that no user object was created

To run this test case:

  • Right-click on login.jsp, and run it. This will update the Web application in OC4J.
  • Right-click on LoginActionTester, and run it "as a JUnit Test Case".

The end result is seen in the JDeveloper consule, as with the previous example of a simple JUnit test.

Let's add the Test Case to the AllTests Test suite that was previously created:

  suite.addTestSuite(jdubois.otn.cactus.test.LoginActionTester.class); 

Relaunch OC4J, and run "AllTest". Figure 4 is the end result:

Figure 4: TestingYour Code

This example mixed some JUnit methods (such as assertTrue() ) and some StrutsTestCase methods (such as verifyForward() ). As StrutsTestCase is an extension of JUnit, everything that works with JUnit also works for J2EE unit tests.

The methods used for testing the LoginAction are pretty simple, but they are enough to get us started. They provide enough functionality to verify that a Struts action performs correctly.

Testing the Generated HTML with HttpUnit

Let's test again a successful login, this time using HttpUnit. As explained in the installation part of this article, HttpUnit does not play well with Cactus. The best way to write this test is to temporarily remove the Cactus libraries from your Oracle JDeveloper project, in order to avoid any classloading errors.

HttpUnit is not a JUnit extension per se, as Cactus is. In a sense, its name is rather misleading, as all it provides is a set of classes that can act as a browser: they handle forms, tables, even JavaScript and cookies. But those classes, when used with JUnit, can become a powerful black-box testing mechanism, as can be seen in the next example, where HttpUnit is used within a standard JUnit test case:

  package jdubois.otn.cactus.test; import junit.framework.*; import com.meterware.httpunit.*; public class LoginActionHttpTester extends TestCase { public LoginActionHttpTester(String sTestName) { super(sTestName); } public void testSuccessfulLogin() throws Exception { WebConversation wc = new WebConversation(); WebRequest req = new GetMethodWebRequest("http://127.0.0.1:8988/your-project-context-root/login.jsp"); WebResponse resp = wc.getResponse(req); assertTrue(resp.getText().indexOf("Welcome to our online discussion board") > 0); WebForm form = resp.getFormWithName("loginForm"); form.setParameter("name", "Julien"); form.submit(); resp = wc.getCurrentPage(); assertEquals("Discussion", resp.getTitle()); } public static void main(String[] args) { junit.textui.TestRunner.run(MessageTester.class); } } 

This code is a standard JUnit test case, which can be added to a test suite as any normal test case. Of course, HttpUnit will work only if the Web application is correctly deployed on the application server. The best way to achieve this feature is to keep an instance of the application server running and hot-redeploy the tested applications at runtime, before running the JUnit tests. With JBoss, this means copying the generated war/ear to the deploy/directory and checking whether the login.jsp page is available. One can argue that hot-redeploying all the time can wreak havoc on an application server, but in this author's experience you can do this hundreds of times before hurting an application server—today's hot-deployment techniques are sufficiently mature to cope with this matter.

As we will see later, this kind of problem does not exist with Cactus, which provides its own Ant task for dealing with this problem. In the future Cactus' author, Vincent Massul, will probably release a generic framework for starting/stopping application servers, which will come in handy for using HttpUnit.

Choosing between Cactus and HttpUnit. As we have seen, the same kind of tests can be provided by Cactus and HttpUnit. HttpUnit is more a black-box testing tool. It is a language-agnostic testing tool (you could test PHP pages with it, for example). It is also easier to understand and use for testers. But as long as you want to use J2EE technulogy, and especially Struts actions, Cactus is the better tool. For example, StrutsTestCase's treatment of ActionErrors is extremely efficient. It will also require a lot less maintenance, as tests will not depend on the HTML presentation layer.

Using Extreme Programming with the AddMessageAction

The LoginAction was developed in a very classic way: first the developer codes the application, and then the tester writes JUnit tests. Today, a different approach exists: Extreme Programming (XP) and test-first development.

In XP, the developer writes the tests before coding anything else. This is a very modern and popular technique, and it is very customer-focused. The best way to get started with XP is probably to read Kent Beck's excellent "Extreme Programming Explained," but many other books have been published on this subject, and the XP community provides a lot of documentation on the internet.

So let's start with the usual question: What do we want to do?

  • Create an action that adds a new message.
  • If the user is not logged in, forward the user to the login page.
  • If the user didn't write a comment, just refresh the current page.

Here is the code of the JUnit test case:

AddMessageActionTester.java

  package jdubois.otn.cactus.test; import java.util.Collection; import servletunit.struts.CactusStrutsTestCase; public class AddMessageActionTester extends CactusStrutsTestCase { public AddMessageActionTester(String testName) { super(testName); } public void testAddMessage() { request.getSession().setAttribute("user", "Julien"); addRequestParameter("comment","First post!"); setRequestPathInfo("/AddMessage"); actionPerform(); verifyForward("success"); verifyForwardPath("/messages.jsp"); assertEquals("Julien", getSession().getAttribute("user")); Object messages = request.getSession() .getServletContext().getAttribute("messages"); assertNotNull(messages); if(!(messages instanceof Collection)) { fail("Messages should be a Collection."); } verifyNoActionErrors(); } public void testAddMessageNoUser() { addRequestParameter("comment","First post!"); setRequestPathInfo("/AddMessage"); actionPerform(); verifyForward("login"); verifyForwardPath("/login.jsp"); verifyNoActionErrors(); } public static void main(String[] args) { junit.textui.TestRunner.run(AddMessageActionTester.class); } } 

Add this test case to the "AllTests" Test Suite, restart OC4J, and launch the Test Suite.

The whule application compiles and deploys; however, the new tests fail as expected. So it is possible to create test cases even before coding a single action.

This technique can be used for large scale; in this way, JUnit is used as a light project management tool. JUnit can produce reports that give an indication of the project's state. For example, in our case we have two failures out of five tests, so our project is—very roughly—at 60-percent completion.

Let's finish our work and code the AddMessageAction:

  package jdubois.otn.cactus.web; import java.util.Collection; import javax.servlet.http.*; import org.apache.struts.action.*; import jdubois.otn.cactus.domain.Message; public class AddMessageAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { if (request.getSession().getAttribute("user") == null || request.getSession().getServletContext().getAttribute("messages") == null) { return mapping.findForward("login"); } String user = (String) request.getSession().getAttribute("user"); MessageForm frm = (MessageForm) form; if (frm.getComment() != null && !frm.getComment().equals("")) { Message message = new Message(user); message.setComment(frm.getComment()); Collection messages = (Collection) request.getSession() .getServletContext().getAttribute("messages"); messages.add(message); } frm.setComment(""); return mapping.findForward("success"); } } 

MessageForm:

  package jdubois.otn.cactus.web; import org.apache.struts.action.*; import javax.servlet.http.HttpServletRequest; public class MessageForm extends ActionForm { private String user; private String comment; public void setUser(String user) { this.user = user; } public String getUser() { return user; } public void setComment(String comment) { this.comment = comment; } public String getComment() { return comment; } } 

The action needs to be called from a JSP page, so this must be added to messages.jsp:

  <html:form action="/AddMessage"> <P>User : <bean:write name="user"/> </P> <P> <html:textarea property="comment"/> </P> <P> <html:submit value="Add a new message"/> </P> </html:form> 

Now, right-click on login.jsp, and select "run". The application should work completely, and you can invite some fellow developers to talk with you on the discussion board (see Figure 5).

Figure 5: Discussion Board Application Snapshot

Congratulations! The whule test suite should now succeed, as all five JUnit tests are successful. Remember this handy saying, "Keep the bar green to keep the code clean."

Here are some excellent sources of further information on this topic:

  • Read the different projects' documentation.
  • Subscribe to the projects' users' mailing lists, which are very active.
  • Buy some specialized books—for example, Vincent Massol's top-notch "JUnit in Action."

Next Steps

The problem with Struts, and testing EJBs. Struts actions should be used only as glue between the presentation layer and the business layer. A common mistake is to include business logic in Struts actions. Putting business logic into Session Bean EJBs allows for better reusability of the code and looser coupling between the different layers.

If the business logic is pushed away from Struts actions, then:

  • StrutsTestCase becomes more of a black-box testing tool. Actions are tested without your knowing what's running behind them.
  • It becomes useful to test Session Beans directly, in order to do white-box testing.

Testing Session Beans with JUnit is in fact very simple, thanks to their remote interface. JUnit can use Session Beans as regular Java objects, so no supplementary framework is needed in order to invoke them. The code would look like this

  package jdubois.otn.ejb.test; import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; import junit.framework.TestCase; public class SimpleEjbTest extends TestCase { protected SimpleEjbHome ejbHome; protected SimpleEjb simpleEjb; private String name = "ejb/remote/simplebean"; public SimpleEjbTest() throws Exception { InitialContext context = new InitialContext(System.getProperties()); Object obj = context.lookup(name); ejbHome = (SimpleEjbHome)PortableRemoteObject. narrow( obj, SimpleEjbHome.class); simpleEjb = ejbHome.create(); } public void testSimpleMethod () { try{ String result = simpleEjb.sayHello(); assertEquals("Hello", result); } catch (Exception e){ fail("An exception has occured " + e.getClass() + " : "+e.getMessage()); } } } 

This code can be used directly inside JUnit, exactly as in the first example (the Message JavaBean).

Debugging an application with JDeveloper. When a test case fails, using a debugger can help to find what the problem is. The debugger provided with Oracle JDeveloper works perfectly with the current JUnit approach, even when testing server-side components.

In order to use the debugger, choose one line of a Struts action, where the debugging should start. Click on the left of the line; a small red dot should appear. Then right-click on "login.jsp", and select "debug" instead of "run". If the JUnit tests are launched (using the AllTests.java test suite, with the JUnit GUI for example), the test will halt and JDeveloper will go into debug mode at the selected break point (see Figure 6):

Next Steps

    Use the following resources to learn more about Ant and start building and deploying Java projects.

    Ant Industry Trends A behind-the-scenes look into the making of this cross-platform building tool.

    Read more about Ant Integration in JDeveloper Ant integration within Oracle JDeveloper is done by adding an Ant build file to a JDeveloper project or by generating a new Ant build file from an existing JDeveloper project.

    Download Oracle JDeveloper 10g Oracle JDeveloper 10g is an integrated development environment with end-to-end support for modeling, developing, debugging, optimizing, and deploying Java applications and Web services.

    Testdrive: Using Ant for Builds This viewlet demonstrates how users with existing Ant projects can use these projects within JDeveloper.

    Getting started with Ant Part 1 Here's to begin using this invaluable tool for building and deploying Java projects. This article coveres some basic Ant tasks that you would likely perform during the Java development process.

    Getting started with Ant Part 2 Here's Part 2 in our series on an invaluable tool for building and deploying Java projects. This article discusses some of Ant's more advanced features that are available in Ant's two task packages: the core task package and the optional task package.

    Command-line Approach to Creating Java Applications on Linux This article serves as a good hands-on guide to developing and deploying Java client applications on Linux using Ant.

    Read more about Ant Visit the official Apache Ant site for more project details.

    Ant for Task Writers Take advantage of the changes in Ant 1.6 internals to write a task or even a library of tasks.

    Related Articles and Downloads

    Blog: Ant Deployment to OC4J

    Blog: How to dynamically retrieve properties from a JDeveloper project file into an ant build file?

    Viewlet: Using CVS for Software Configuration

Figure 6: Debugging with Oracle JDeveloper and JUnit

Oracle JDeveloper IDE uses the integrated OC4J application server and is the easiest way to test and debug an application within JDeveloper. JDeveloper uses Java's standard debugging abilities to achieve this feature, and as such it can debug any standard Java application server. For example, here is a how-to on debugging an application deployed on the JBoss application server from within JDeveloper: .

Using Ant to automate the testing.

The <cactus/> task. The <cactus/> ant task is able to launch an application server and execute the Cactus test case against it, automatically. It is one of the essential tasks to use if you want to set up Continuous Integration of your projects.

Unfortunately, OC4J is not one of the application servers available in the <cactus/> task, but as long as a standard war/ear file is coded, another application server can be used for this purpose. It is possible, according to the Cactus documentation (http://jakarta.apache.org/cactus/integration/ant/task_cactus.html#generic ), to use OC4J with the <cactus/> task. However, OC4J is not a supported platform and, in the author's experience, you might run into problems while stopping OC4J—which will wreak havoc in a Continuous Integration process.

  <mkdir dir="results"/> <cactus warfile="./cactified-discussion-board.war" fork="yes" failureproperty="test.failure"> <classpath refid="classpath"/> <containerset timeout="300000"> <jboss3x dir="${jboss.install.dir}" port="8080"/> </containerset> <formatter type="brief" usefile="false"/> <batchtest todir="results"> <fileset dir="${src.dir}"> <include name=" jdubois/otn/cactus/test/AddMessageActionTester.java"/> <include name=" jdubois/otn/cactus/test/LoginActionTester.java"/> </fileset> </batchtest> </cactus> <fail message="Cactus in-container tests failed." if="test.failure" /> 

The <junitreport/> task. <junitreport/> is a standard Ant task that creates an HTML report of JUnit test results.

Configure JUnit to generate an XML report of the test. For this:

  • Add the <formatter type="xml" /> subelement inside the JUnit or Cactus task.
  • Add the following task to generate the report:

      <junitreport todir="results"> <fileset dir="results"> <include name="TEST-*.xml"/> </fileset> <report todir="results" format="frames"/> </junitreport> 

If you use Continuous Integration on a dedicated machine, you can create a complete HTML report of all JUnit tests, and configure your Web server so that all team members can have access to the complete set of tests results.

Setting up "Continuous Integration." Continuous Integration is part of the Extreme Programming method. With Continuous Integration, tests are run automatically, several times a day. On the author's current project, tests are run every hour. If a bug is found, an error report is sent by e-mail to the members of the team who committed code during that period. One good practice is that when someone breaks down the Continuous Integration, his most urgent task is to correct his code and fix the build.

Continuous Integration can do more than just run JUnit tests. For instance, it can run Checkstyle tests to ensure that all developers are following the coding guidelines of the project (another Extreme Programming good practice). Checkstyle is a SourceForge project:http://checkstyle.sourceforge.net/ .

There are several Continuous Integration tools available:

The author has been using CruiseControl for a long time and heartily recommends it. It's a wonderful tool—very well thought out and easy to set up.

Conclusion

JUnit, server-side testing, test-first development. . . . These are very complex matters, but today a number of tools are available to help developers get past the technical difficulties. Besides, these tools integrate well with Oracle technologies such as JDeveloper and OC4J.

So why not start testing now? It gives you confidence in your application, it gives you the courage to change your code, and it is fun to do .

Julien Dubois is a J2EE expert living in Paris, France. He can be found on the internet at http://www.julien-dubois.com .