Unit Testing

This document outlines two major ways to test your ActionBeans and related classes outside of a container like Tomcat, Resin or Jetty. To decide which way is right for you, you should probably read and examine both - neither is a one-size-fits all solution, and you may even want to apply both techniques in your project.

But first, let's get a few things out of the way...

  1. Automated testing is a good thing(tm). Having automated tests lets you refactor ruthlessly, and generally leads to better quality code.
  2. I don't want to get into a philosophical argument about what is and isn't unit test; having well written tests that can be run automatically is good whether they are technically unit tests or not
  3. There are tools out there that will let you test the whole cycle including JSP rendering inside your regular servlet container; I find these are a bit too much like hard work and prefer to test outside of my container when possible
  4. I'd like to personally recommend TestNG to every Stripes user. It's way ahead of JUnit these days, and if you've done any unit testing in your life, your ramp-up time will be about 15 minutes.

As if to prove point number four, all the examples in this document (indeed all the Stripes unit tests) use TestNG.

Approach 1: Invoking your ActionBeans directly

Because ActionBean classes are simply POJOs there's nothing stopping you instantiating, setting attributes on them, and invoking handler methods. Take the following example test of the CalculatorActionBean class from the examples:

CalculatorActionBeanTest.java
public class CalculatorActionBeanTest { 
	@Test 
	public void myFirstTest() throws Exception { 
		CalculatorActionBean bean = new CalculatorActionBean(); 
		bean.setContext( new ActionBeanContext() ); 
		bean.setNumberOne(2); 
		bean.setNumberTwo(2); 
		bean.add(); 
		Assert.assertEquals(bean.getResult(), 4, "Oh man, our math must suck!"); 
	} 
} 

This works really well when ActionBeans are standalone entities. But if we want to handle situations where values are stored in session, or cookies are set using the response, we need to go one further. Hopefully by now you've read the How-To on State Management; if not you might like to skim it now. It explains how to use your own subclass of ActionBeanContext with Stripes to make state management clean, type safe and independent of the Servlet API. Let's imagine we had the following abstract ActionBeanContext subclass to define our interface:

MyAbstractActionBeanContext.java
public class MyAbstractActionBeanContext extends ActionBeanContext { 
	public abstract void setUser(User user); 
	public abstract User getUser(); 
} 

The concrete implementation of this class that is used by the regular application would look like this:

MyActionBeanContext.java
public class MyActionBeanContext extends MyAbstractActionBeanContext { 
	public void setUser(User user) { 
		getRequest().getSession().setAttribute("user", user); 
	} 

	public User getUser() { 
		return (User) getRequest().getSession().getAttribute("user"); 
	} 
} 

So far so good right? Now, if all our ActionBeans are coded against the MyAbstractActionBeanContext class, then they'll accept any subclass of it, not just the MyActionBeanContext class that we'll be using in the regular application. That means we can write a test implementation and substitute that in during testing. Such an implementation might look like:

MyTestActionBeanContext.java
public class MyTestActionBeanContext extends MyAbstractActionBeanContext { 
	private Map<String,Object> fakeSession = new HashMap<String,Object>(); 

	public void setUser(User user) { 
		this.fakeSession.put("user", user); 
	} 

	public User getUser() { 
		return (User) this.fakeSession.get("user"); 
	} 
} 

Note that we could have just used an attribute of type User in the test implementation, but if you're going to store more than a couple of objects in session, using a Map is probably easier. Although the above only demonstrates wrapping session, the same pattern can be applied for interaction with any Servlet API, e.g. setting and retrieving cookies, setting request attributes etc. If you mediate all access to Servlet API classes through a custom ActionBeanContext then your ActionBeans can have the best of both worlds - use of the Servlet API when they need it, and complete independence from the Servlet API!

The following shows how we might use this technique to perform a couple of tets on a LoginActionBean:

LoginActionBeanTest.java
public class LoginActionBeanTest { 
	@Test 
	public void successfulLogin() throws Exception { 
		MyAbstractActionBeanContext ctx = new MyTestActionBeanContext(); 
		LoginActionBean bean = new LoginActionBean(); 
		bean.setContext(ctx); 
		bean.setUsername("shaggy"); 
		bean.setPassword("shaggy"); 
		bean.login(); 

		Assert.assertNotNull(ctx.getUser()); 
		Assert.assertEquals(ctx.getUser().getFirstName(), "Shaggy"); 
		Assert.assertEquals(ctx.getUser().getLastName(), "Rogers"); 
	} 

	@Test 
	public void failedLogin() throws Exception { 
		MyAbstractActionBeanContext ctx = new MyTestActionBeanContext(); 
		LoginActionBean bean = new LoginActionBean(); 
		bean.setContext(ctx); 
		bean.setUsername("shaggy"); 
		bean.setPassword("scooby"); 
		bean.login(); 

		Assert.assertNull(ctx.getUser()); 
		Assert.assertNotNull(ctx.getValidationErrors()); 
		Assert.assertEquals(ctx.getValidationErrors().get("password").size(), 1); 
	} 
} 

This approach is all well and good, but it has several shortcomings:

  • It doesn't test the URL bindings of our classes
  • It doesn't test that the validations and type conversions are working correctly
  • As ActionBeans get more complex your tests look more and more like the container
  • It is hard to test that the Resolutions are working correctly

Approach 2: Mock Container Usage

Starting with version 1.1.1 Stripes comes packaged with a rich set of mock objects that implement a large number of the interfaces in the Servlet specification. These can be used to construct a mock container in which to process requests. While a little more complex, this more closely simulates the environment in which your ActionBeans will execute. It also has the benefit of testing everything you specified in annotations.

The classes in question can be found in the package net.sourceforge.stripes.mock. Over time you may want to become familiar with most of these classes, but for now can focus on just two: MockServletContext and MockRoundtrip.

MockServletContext is a mock implementation of an individual context within a servlet environment. We'll set up a class of this type to accept and process our requests. It takes very little code to get started:

Setting up a MockServletContext
MockServletContext context = new MockServletContext("test"); 

// Add the Stripes Filter 
Map<String,String> filterParams = new HashMap<String,String>(); 
filterParams.put("ActionResolver.Packages", "net.sourceforge.stripes"); 
context.addFilter(StripesFilter.class, "StripesFilter", filterParams); 

// Add the Stripes Dispatcher 
context.setServlet(DispatcherServlet.class, "StripesDispatcher", null); 

In this example we're doing the following:

  • Instantiating a MockServletContext and giving in the name test (i.e. all URLs will start /test )
  • Setting some initialization parameters for the StripesFilter
  • Inserting the StripesFilter into the mock context
  • Registering the Stripes DispatcherServlet with the mock context

At this point you might be asking yourself why you have to replicate these steps? Well, umm, ok. It's only five lines of code without the comments and empty lines. The reason is simply that the mock objects included with Stripes are not tied to Stripes in any way. If you have a couple of custom servlets you wrote (and why would you do that when you're using Stripes? but I digress) you could test those just as effectively using these mock objects.

The initialization parameters supplied to the StripesFilter above are the same as those that would appear in the web.xml for use in a container. You're free to supply identical values, or values more appropriate for testing. It's up to you.

Adding additional filters

You can add any additional filters needed to make sure your ActionBeans function correctly. If you use a Filter to implement the OpenSessionInView pattern, or to roll your own security, you can add it to the context too. Just remember that filters are invoked in the order in which they are inserted into the context.

Being mock objects, and not a full servlet container, there are several limitations of which you should be aware:

  • There's no URL matching; all Filters are applied to every request
  • A MockServletContext supports only a single Servlet, so you can only test on thing at a time
  • Forwards, Redirects and Includes are not processed (but information about them is recorded for verification)

Lastly, before we move on, it's probably a good idea to take the code that generates the MockServletContext and wrap it up in a test fixture. While instantiating a mock context is much cheaper than starting up a full servlet container, it still can take a second or two. That's not much, but if you manufacture one for each of your unit test methods, and you have hundreds of test methods...well, you'll be waiting a while for those tests to run. Such a test fixture could be implemented either as a regular class which lazily creates a single mock context and provides it to any test that needs it, or by annotating the method that generates the context with TestNG's @Configuration(beforeSuite=true) annotation. For example:

TestFixture.java
public class TestFixture { 
	private static MockServletContext context; 

	@Configuration(beforeTest=true) 
	public void setupNonTrivialObjects() { 
		TestFixture.context = new MockServletContext("test"); 
		... 
	} 

	public static MockServletContext getServletContext() { 
		return TestFixture.context; 
	} 
} 

Now, on to the interesting bit - writing some actual tests. While you're certainly welcome to go about instantiating and using all the classes in the mock package directly, it's much easier to use MockRoundtrip instead. MockRoundtrip acts as a facade to several other mock objects and introduces some knowledge of how Stripes works in order to make things as simple as possible. The following is a basic example of a test using MockRoundtrip:

Simple tests using MockRoundtrip
@Test 
public void positiveTest() throws Exception { 
	// Setup the servlet engine 
	MockServletContext ctx = TestFixture.getServletContext(); 

	MockRoundtrip trip = new MockRoundtrip(ctx, CalculatorActionBean.class); 
	trip.setParameter("numberOne", "2"); 
	trip.setParameter("numberTwo", "2"); 
	trip.execute(); 

	CalculatorActionBean bean = trip.getActionBean(CalculatorActionBean.class); 
	Assert.assertEquals(bean.getResult(), 4.0); 
	Assert.assertEquals(trip.getDestination(), "/index.jsp"); 
} 

There's quite a lot going on in that little snippet. First we fetch the MockServletContext from the test fixture. Next we instantiate a new MockRoundtrip passing it the context and the ActionBean class we want to invoke. The context is used to help generate the URL for the request, and is used later on to process the request. The ActionBean class is used solely to grab the URL from the @UrlBinding annotation. If you prefer, there are alternate constructors that take String URLs instead. Also, since we didn't pass in a MockHttpSession, the MockRoundtrip will create one for us. For larger/longer tests you might like to create a MockHttpSession and use it for several requests to mimic a real session.

After the MockRoundtrip is created, we set two request parameters on it. Notice how they are both Strings just like any regular request parameter. If you need to supply more than a single value for any parameter you can just supply additional values - the setParameter() and addParameter methods are vararg methods.

trip.execute() executes the request in the servlet context that was supplied when we constructed the MockRoundtrip. This will (assuming our test works) invoke the default operation on the CalculatorActionBean. There is an alternative execute() method that takes an event name as a parameter, and will format the request as if the requested event had been submitted.

Lastly comes verification. We fetch the ActionBean instance from the MockRoundtrip. To be clear: this is an ActionBean that was instantiated by Stripes and just handled a request! The first assertion is quite simple. It checks to ensure that the correct result was calculated (the default operation is addition). The second assertion checks that the ActionBean forwarded or redirected the user to the correct page. One important thing to note is that the MockRoundtrip will paths used in forwards and redirects into web-application-relative paths. You'll get the same path back whether the request resulted in a forward or a redirect, and it will never include the context path. This just makes for easier testing if you switch back and forth between the two resolutions.

Testing failure cases is just as easy:

Testing failure cases with MockRoundtrip
@Test 
public void negativeTest() throws Exception { 
	// Setup the servlet engine 
	MockServletContext ctx = TestFixture.getServletContext(); 

	MockRoundtrip trip = new MockRoundtrip(ctx, CalculatorActionBean.class); 
	// Omit first parameter - we could also have set it to "" 
	trip.setParameter("numberTwo", "abc"); 
	trip.execute(); 

	CalculatorActionBean bean = trip.getActionBean(CalculatorActionBean.class); 
	Assert.assertEquals(bean.getContext().getValidationErrors().size(), 2); 
	Assert.assertEquals(trip.getDestination(), MockRoundtrip.DEFAULT_SOURCE_PAGE); 
} 

This looks very similar, except that we are omitting a required parameter and supplying "abc" for a numeric field. The first assertion checks to make sure that we have two entries in ValidationErrors. The structure of ValidationErrors is a Map of fieldName to List<ValiationError>. If we wanted to be doubly sure we could fetch the list of errors for each field and make sure each field got an error.

The second assertion checks that the request resulted in navigation back to the originating page. Stripes requires requests to supply the path to the page from which they came (at least if the request could generate validation errors). This is usually taken care of by the <stripes:form> tag, but in this case the MockRoundtrip inserted a default value for us. If we cared about the value, we could set it with MockRoundtrip.setSourcePage(String url).

Lastly, we might have handler methods in ActionBeans that stream data back instead of sending the user to a JSP. For example, the following exerpt from the AJAX CalculatorActionBean:

Handler method that streams data to the client
@HandlesEvent("Addition") @DefaultHandler 
public Resolution addNumbers() { 
	String result = String.valueOf(numberOne + numberTwo); 
	return new StreamingResolution("text", new StringReader(result)); 
} 

In this case we stream back a tiny amount of data - a single floating point number. But the principle is the same as if we streamed back reams of XML or JavaScript. We can test this as follows:

Testing ActionBeans that stream output
@Test 
public void testWithStreamingOutput() throws Exception { 
	MockServletContext ctx = TestFixture.getServletContext(); 

	MockRoundtrip trip = new MockRoundtrip(ctx, CalculatorActionBean.class); 
	trip.setParameter("numberOne", "2"); 
	trip.setParameter("numberTwo", "2"); 
	trip.execute("Addition"); 

	CalculatorActionBean bean = trip.getActionBean(CalculatorActionBean.class); 
	Assert.assertEquals(bean.getResult(), 4.0); 
	Assert.assertEquals(trip.getOutputString(), "4.0"); 
	Assert.assertEquals(trip.getValidationErrors().size(), 0); 
	Assert.assertNull(trip.getDestination()); 
} 

The key line here is the second assertion. This tests that the String output by the ActionBean is exactly equal to "4.0". We could of course test larger strings by checking that they contain certain patterns, or known data. Also MockRoundtrip has a getOutputBytes() method that can be used to retrieve a byte[] of output in case the ActionBean's output was not text.

For more information on using MockRoundtrip and the other mock objects please refer to the javadoc.

MockRoundtrips are compatible with Spring !

Now that you want to use MockRoundtrip with your spring/stripes application, you've got a nice exception :

Exception with Spring
15:01:19,277 WARN DefaultExceptionHandler:90 
- Unhandled exception caught by the Stripes default exception handler. 
net.sourceforge.stripes.exception.StripesRuntimeException: 
Exception while trying to lookup and inject a Spring bean into a bean of type MyActionBean using field access on field private 
com.xxx.service.MySpringBean 
com.xxx.action.GenericActionBean.mySpringBean 

To fix it, you just have to change a little bit your MockServletContext initialisation...

Adding Spring compatibility
private static MockServletContext context; 

@Before 
public static void initContext() { 
	Map<String, String> filterParams = new HashMap<String, String>(); 
	//add stripes extensions 
	filterParams.put("Interceptor.Classes", "net.sourceforge.stripes.integration.spring.SpringInterceptor");

	filterParams.put("ActionResolver.Packages", "net.sourceforge.stripes"); 

	context.addFilter(StripesFilter.class, "StripesFilter", filterParams); 

	//here goes your own configuration file 
	context.addInitParameter("contextConfigLocation", "/WEB-INF/applicationContext.xml"); 

	// bind your context with an initializer 
	ContextLoaderListener springContextListener = new ContextLoaderListener(); 
	springContextListener.contextInitialized(new ServletContextEvent(context)); 

	// Add the Stripes Dispatcher 
	context.setServlet(DispatcherServlet.class, "StripesDispatcher", null); 
} 

See also :

Wizard action testing using approach 2: Mock Container Usage

Wizard action testing is tricky, so we should make additional efforts to run test for it. You are setting up mockup MockServletContext as usually.

Setting up a MockServletContext
private MockServletContext ctx; 
.......... 
@Before 
public void setUpMockServletContext() { 
	ctx = new MockServletContext("test"); 

	// Add the Stripes Filter 
	Map<String,String> filterParams = new HashMap<String,String>(); 
	filterParams.put("ActionResolver.Packages", "com.yourpackage.action"); 
	// filterParams.put("LocalePicker.Locales", "....."); 
	// filterParams.put("Extension.Packages", "com.yourpackage.action.extention"); 
	ctx.addFilter(StripesFilter.class, "StripesFilter", filterParams); 
	// Add the Stripes Dispatcher 
	ctx.setServlet(DispatcherServlet.class, "StripesDispatcher", null); 
} 

As we can see we have only one filter in current configuration. We'll use that fact a bit later.

Say you have following fields in your Wizard action bean used on several pages of Wizard process.
The fields are field1, field2, field3.

You can try run your test as before.

Unhandled exception in test method
@Test 
public void negativeTest() throws Exception { 
	MockRoundtrip trip = new MockRoundtrip(ctx, MyWizardActionBean.class); 
	trip.setParameter("field1", "abc"); 
	trip.execute(); 
	// ....... the rest of test code 
} 

Your most probable result is getting error like that.

Unhandled exception message
WARN net.sourceforge.stripes.exception.DefaultExceptionHandler - Unhandled exception caught by the Stripes default exception handler. 
net.sourceforge.stripes.exception.StripesRuntimeException: Submission of a wizard form in Stripes absolutely 
requires that the hidden field Stripes writes containing the names of the fields present on the form is present 
and encrypted (as Stripes write it). This is necessary to prevent a user from spoofing the system and getting 
around any security/data checks. 

That happens because of Wizard action special processing made by Stripes. The work around is adding following code into your test method.

Work around for Unhandled exception in Wizard action
import net.sourceforge.stripes.util.CryptoUtil; 
....... 
@Test 
public void negativeTest() throws Exception { 
	MockRoundtrip trip = new MockRoundtrip(ctx, MyWizardActionBean.class); 
	trip.setParameter("__fp", CryptoUtil.encrypt("||field1||field2||field3"));// used for @Wizard action 
	trip.setParameter("field1", "abc"); 
	trip.execute(); 
	// ....... the rest of test code 
} 

Field encryption code should start with two leading || and separate all fields by another ||. Then encrypted value is assigned to special parameter "__fp" used internally by Stripes.

Now all your tests pass quit well but most probably you'll find following error in your test log.

Error in Wizard action test log
ERROR net.sourceforge.stripes.controller.StripesFilter - net.sourceforge.stripes.exception.StripesRuntimeException: 
Something is trying to access the current Stripes configuration but the current request was never routed through
the StripesFilter! As a result the appropriate Configuration object cannot be located. Please take a look at the 
exact URL in your browser's address bar and ensure that any requests to that URL will be filtered through the 
StripesFilter according to the filter mappings in your web.xml. 

You should add one more JUnit fixture method in your test to work around that issue. You need to remove Stripes filter in your MockServletContext and set it up again for every test method. Add following code to your test and try it again.

Work around second exception in Wizard action log
@After 
public void cleanUp() { 
	// destroy Stripes filter for every test method 
	ctx.getFilters().get(0).destroy(); // assume you have only one (first and single) filter in config 
} 

We are destroying Stripes filter in internal configuration after every test method. That code assumes you have only one filter in test, so you have them several you should adjust that code for your case.