Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

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:

Code Block
languagejava
titleCalculatorActionBeanTest.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:

Code Block
languagejava
titleMyAbstractActionBeanContext.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:

Code Block
languagejava
titleMyActionBeanContext.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:

Code Block
languagejava
titleMyTestActionBeanContext.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"); 
	} 
} 

...

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

Code Block
languagejava
titleLoginActionBeanTest.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); 
	} 
} 

...

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:

Code Block
languagejava
titleSetting 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); 

...

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:

Code Block
languagejava
titleTestFixture.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:

Code Block
languagejava
titleSimple 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"); 
} 

...

Testing failure cases is just as easy:

Code Block
languagejava
titleTesting 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); 
} 

...

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:

Code Block
languagejava
titleHandler 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:

Code Block
languagejava
titleTesting 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()); 
} 

...

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

Code Block
languagetext
titleException 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...

Code Block
languagejava
titleAdding 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); 
} 

...

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

Code Block
languagejava
titleSetting 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); 
} 

...

You can try run your test as before.

Code Block
languagejava
titleUnhandled 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.

Code Block
languagetext
titleUnhandled 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.

Code Block
languagejava
titleWork 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 
} 

...

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

Code Block
languagetext
titleError 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.

Code Block
languagejava
titleWork 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 
} 

...