Saturday, August 18, 2007

Testing Portlets with Jetty, Pluto and JWebUnit

After my last two entries, I've gotten some questions about using pluto embedded in jetty to create automated integration tests for JSR 168 portlets. Using the maven-jetty-plugin for running the portlets is great for fast, iterative development. But it can't be used to run automated integration tests. Remembering an excellent article from Johannes Brodwall's blog about integration testing with Jetty and JWebUnit, I wanted to extend his approach to use the embedded jetty-pluto setup I have created. This turned out to be to be quite easy.

To illustrate the solution, let's start from scratch, creating a simple portlet displaying a personalized "Hello World" message (how original...). Note that this is only a very simple example, outlining how it is possible to set up the infrastructure for simple web integration testing of portlets.

The first step is to create a blank portlet project, using Maven 2 and the portlet archetype:


mvn archetype:create
-DgroupId=com.mycompany.app
-DartifactId=my-portlet
-DarchetypeArtifactId=maven-archetype-portlet


(Type this on one line)

I'll use Eclipse for this example, so I'll set up an Eclipse project for the portlet by typing mvn eclipse:eclipse. Now you can go ahead importing the project into Eclipse as an existing project.

Unfortunately, it appears that the portlet arhcetype contains a bug, so make sure you update the portlet-class element in the portlet.xml file with the correct portlet class:

<portlet-class>com.mycompany.app.MyPortlet</portlet-class>


Then we need some dependencies to Jetty, JWebUnit and the maven-jetty-pluto-embedded artifact, so add these to the dependencies section of the pom.xml file of the portlet project:


Update: The maven-jetty-pluto-embedded artifact is available in the Maven 2 repositories.



<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>6.1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jsp-2.1</artifactId>
<version>6.1.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.jwebunit</groupId>
<artifactId>jwebunit-htmlunit-plugin</artifactId>
<version>1.4.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.bekk.boss</groupId>
<artifactId>maven-jetty-pluto-embedded</artifactId>
<version>1.0</version>
<scope>test</scope>
</dependency>


(The JWebUnit dependency has an indirect dependency on commons-logging, which for some weird reason has a dependency on the servlet-api!? This version of the servlet-api causes problems for the version of jetty we're running, so we have to exclude it)

Run mvn eclipse:eclipse again to download the new dependencies. If you want to know what the maven-jetty-pluto-embedded artifact is, you should read my previous entries).

Open the MyPortlet portlet that was generated for us, and remove everything but the method signature for the doView() method, like this:


public class MyPortlet extends GenericPortlet {

public void doView( RenderRequest request, RenderResponse response ) {
}

}


Now we'll start preparing the test, and this is where the fun begins. We'll have to configure Jetty in our test to use the embedded jetty pluto setup from the previous articles. Create a new source folder, src/test/java, and create a new blank WebTestCase:

package com.mycompany.app;

import net.sourceforge.jwebunit.junit.WebTestCase;

public class MyPortletIntegrationTest extends WebTestCase {

}


We'll have to create a setUp() metohd that initializes the Jetty server, and configures it with the pluto driver:


protected Server server;

public void setUp() throws Exception {
System.setProperty("org.apache.pluto.embedded.portletId", "my-portlet");
server = new Server(8080);
WebAppContext webapp = new WebAppContext("src/main/webapp", "/test");
webapp.setDefaultsDescriptor("/WEB-INF/jetty-pluto-web-default.xml");
ServletHolder portletServlet = new ServletHolder(new PortletServlet());
portletServlet.setInitParameter("portlet-name", "my-portlet");
portletServlet.setInitOrder(1);
webapp.addServlet(portletServlet, "/PlutoInvoker/my-portlet");
server.addHandler(webapp);
server.start();
getTestContext().setBaseUrl("http://localhost:8080/test");
}

public void tearDown() throws Exception {
server.stop();
}


So what does all this mean? Basically, we have just copied the setup from the configuration of the maven-jetty-plugin that we used earlier, and configured the Jetty server programatically in our integration test. In addition, we've added and configured an instance of the pluto PortletServlet so we don't have to modify our web.xml file (which normally would be something our build takes care of). We also added a tearDown() method to gracefully shut down the server.

Now it's time to write an actual test. We'll start very, very simple, by just testing if the portlet displays a simple message when it is displayed normally in the view mode:


public void testIndexPageDisplaysMessage() throws Exception {
// Start at the pluto driver entry point
beginAt("/pluto/index.jsp");
assertTextPresent("Hello World from MyPortlet!");
}


Running the test, it will of course fail since we have not implemented anything in the portlet yet. So let's create an index.jsp file in the /WEB-INF folder of the portlet:


<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>

Hello World from MyPortlet!


In the doView() method of the portlet, we'll just dispatch to this jsp:


public void doView( RenderRequest request, RenderResponse response ) throws IOException, PortletException {
getPortletContext().getRequestDispatcher("/WEB-INF/index.jsp").include(request, response);
}


Now, run the test again. This time, it should show a green bar! That was quick and simple! Let's try to complicate the case a little bit by adding and working with a form. The form should be a form with two text fields, one named firstName and a second named lastName. When submitting the form, the user should be displayed a personalized "Hello World" message:


public void testPersonalizedHelloWorldMessage() throws Exception {
// Start at the pluto driver entry point
beginAt("/pluto/index.jsp");
// Expect a form with name "helloWorldForm" present on the page
assertFormPresent("helloWorldForm");
// This form should have two text fields. We'll populate these with data
setWorkingForm("helloWorldForm");
setTextField("firstName", "Donald");
setTextField("lastName", "Duck");
submit();
assertTextPresent("Quack quack, Donald Duck!");
}


We populate the form with some data, submit it, and verify the result. Running the test now, you'll see it fails, as expected. So let's implement the expected functionality. Normally, I would use a MVC framework with portlet support, such as Struts 2 or Spring MVC, but since I'm only using the core APIs in this example, we'll have to use the portlet as a very basic controller. The portlet will handle the form submits and the logic for including the correct jsp. Let's start by adding the form to the index.jsp page:


<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>

Hello World from MyPortlet!

<form name="helloWorldForm" action="<portlet:actionURL/>" method="POST">
<b>First name</b>: <input type="text" name="firstName"/><br/>
<b>Last name</b>: <input type="text" name="lastName"/><br/>
<input type="submit"/>
</form>


Then we'll implement the processAction() method and process the incoming form submit:


public void processAction(ActionRequest actionRequest, ActionResponse actionResponse) throws PortletException, IOException {
String firstName = actionRequest.getParameter("firstName");
String lastName = actionRequest.getParameter("lastName");
actionResponse.setRenderParameter("firstName", firstName);
actionResponse.setRenderParameter("lastName", lastName);
actionResponse.setRenderParameter("action", "viewResult");
}


Then we'll add some "routing logic" to the doView() method so it will dispatch us to the correct view:


public void doView( RenderRequest request, RenderResponse response ) throws IOException, PortletException {
String action = request.getParameter("action");
if("viewResult".equals(action)) {
getPortletContext().getRequestDispatcher("/WEB-INF/hello.jsp").include(request, response);
}
else {
getPortletContext().getRequestDispatcher("/WEB-INF/index.jsp").include(request, response);
}
}


And finally, the /WEB-INF/hello.jsp file that generates the output:


<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet"%>
<portlet:defineObjects/>

Quack quack, <%=renderRequest.getParameter("firstName") %> <%=renderRequest.getParameter("lastName") %>!


Run the test again, and it should become green!

Next, let's assume that the portlet should do something useful in the edit portlet mode. But how can we simulate a click on the edit button? After some looking though the source of the generated pluto html, I found the XPath expressions that identifies each of the portlet mode and window state buttons. With this information, let's write a test that switches the portlet to edit mode, and assert that we get to the correct page:


public void testSwitchToEditMode() throws Exception {
beginAt("/pluto/index.jsp");
switchEdit();
assertTextPresent("This is MyPortlet in edit mode!");
}

private void switchEdit() {
clickElementByXPath("//span[@class='edit']/..");
}


The switchEdit() method simulates a click on the edit mode control of the portlet, using the correct XPath expression.

Try to run the test, and it will turn red with a message stating that "Unable to locate element with XPath...". This is because pluto does not display the edit link for the portlet. Looking in the portlet.xml for the portlet, you can see that currently the only supported portlet mode is VIEW. So let's add EDIT to the supported modes as well:


<supports>
<mime-type>text/html</mime-type>
<portlet-mode>VIEW</portlet-mode>
<portlet-mode>EDIT</portlet-mode>
</supports>


Running the test again, it will fail, but now with a different error message, stating that the "Expected text was not found". So it's time to implement the doEdit() method and the corresponding JSP. Create an edit.jsp file in the /WEB-INF folder, which just contains

This is MyPortlet in edit mode!


In the portlet, the doEdit() method should look like this:


public void doEdit(RenderRequest request, RenderResponse response) throws IOException, PortletException {
getPortletContext().getRequestDispatcher("/WEB-INF/edit.jsp").include(request, response);
}


This time when you run the test, it should turn green.

As you can see, writing integration tests for a portlet is pretty simple, using the right tools. But why this approach, instead of just running JWebUnit (or Selenium or <insert your favourite web testing framework here>) against a running portal server with the portlet installed? First of all, using this "in process" approach is quicker! It doesn't require you to start a big, heavy portal server, and no login is required. Asserting and verifying content is easier since the only portlet generating content is the one you're testing. And since it's in process, it's really simple to debug. Just run the JUnit test in debug mode, and you can debug the portlet as it responds to the request of the web testing framework.

As a final note, I have uploaded an abstract WebTestCase that you can download and use as a base for your integration tests. It has everything set up for starting Jetty with the pluto portal driver, and with helper methods for switching portlet modes and window states.

14 comments:

Anonymous said...

"maven-jett-pluto-embedded" typo?

/Nils-H said...

Thanks! The typo is fixed now.

RR said...

Thanks for this great post. I'm jumping into portlet development and was wondering how I could run tests during my maven2 build using some embedded portal container. Looks like this should do it.

In setUp you have this line:
webapp.setDefaultsDescriptor("/WEB-INF/jetty-pluto-web-default.xml");

I didn't read anywhere in the post where jetty-luto-web-default.xml is provided, and it didn't show-up by default so I snagged the file and put it in there manually.

I'm wondering, now that many months have passed, if you can reproduce your success following everything in the post? I have followed everything exactly as you have suggested but when running the tests continue to experience an HTTP 500 from Pluto:
008-03-27 09:37:42.292::WARN: /test/pluto/index.jsp
org.apache.jasper.JasperException: /pluto/index.jsp(19,58) PWC6188: The absolute uri: http://java.sun.com/jstl/core cannot be resolved in either web.xml or the jar files deployed with this application

Any suggestions?

/Nils-H said...

The error you get is probably due to a bug in an earlier version of the surefire plugin in Maven 2 (http://jira.codehaus.org/browse/SUREFIRE-459). I'm not sure what the most recent version of the surefire plugin is, but setting it to version 2.4.2 worked for me. I'm using this setup for all my portlet development, and it still works just fine. Take a look at this entry as well: http://portletwork.blogspot.com/2007/10/easy-portlet-development-and-reduced.html

RR said...

Thanks Nils, I saw that issue also and wondered if it was related to this testing. In any case, I do have my dependency set to use version 2.4.2 and I'm still haven the same problem. Do you happen to have posted a tarball or zip of the test project you outlined? Perhaps I have something mis-configured and I could reference your setup.

/Nils-H said...

Have you tried an earlier version of the plugin? 2.3.something? If that doesn't help, please post on the struts user mailing list for further assistance.

Rice said...

I have no problem with running the test in eclipse environment. However, I have similar problem as the above when running 'mvn test' in command line. The error is as follows:

Caused by: com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException: 500 plutoindexjsp1958 PWC6188 The absolute uri httpjavasuncomjstlcore cannot be resolved in either webxml or the jar files deployed with this application for http://localhost:8081/test/pluto/index.jsp
at com.gargoylesoftware.htmlunit.WebClient.throwFailingHttpStatusCodeExceptionIfNecessary(WebClient.java:535)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:332)
at com.gargoylesoftware.htmlunit.WebClient.getPage(WebClient.java:388)
at net.sourceforge.jwebunit.htmlunit.HtmlUnitTestingEngineImpl.gotoPage(HtmlUnitTestingEngineImpl.java:211)
... 31 more

Nils-Helge Garli Hegvik said...

That's probably because you have conflicting versions of the servlet-api on your maven build path. Run maven with debug output and check your dependency graph for different servlet-api versions.

Rice said...

After a while of study, the problem is solved by configuring the parameter useSystemClassLoader to falsefor maven-surefire-plugin. Refers to http://maven.apache.org/plugins/maven-surefire-plugin/examples/class-loading.html

Raja Nagendra Kumar said...

Hi,

Has any one tried testing Linkedn profile data using JWebUnit

referencejavaportal said...

Nice Post. Very Interesting.
I have a question. can i use Jwebunit for websphere portal specific portlets. i was trying to do that by changing the server = new Server(8080); to server = new Server(10040); and
getTestContext().setBaseUrl("http://localhost:8080/test");
to
getTestContext().setBaseUrl("http://localhost:10040/");

Please help on this, if somebody had figured out the way to test the websphere portal specific portlets with this example.

Wadouk said...

[humour don't formalize but that express my feeling] Just to say that the graphical part of the site is only craps : i just spend 4 hours to get working the first test because i could not see that I have not the last part of the line code [code]include(request,response)[/code]

Nevermind, i countinue to read and practice your article. Thank's a lot for your precious post.

QA Testing Tools said...

This is a Great article, thank you guys for sharing.
I also added a link to it at http://www.qatestingtools.com/sourceforge/jwebunit

Jeff said...

Wow. This is the best introduction to portlet programming I've read. I've been trying to repair some legacy portlets for a few months now, and haven't been able to grasp the portlet architecture.

Thanks for writing this.