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...
...
Code Block |
---|
title | 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:
Code Block |
---|
title | MyAbstractActionBeanContext.java |
---|
|
public class MyAbstractActionBeanContext extends ActionBeanContext {
public abstract void setUser(User user);
public abstract User getUser();
}
|
...
Code Block |
---|
title | 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:
Code Block |
---|
title | 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!
...
Code Block |
---|
title | 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:
...
Code Block |
---|
title | 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);
|
...
Code Block |
---|
|
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 |
---|
title | 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");
}
|
...
Code Block |
---|
title | 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);
}
|
...
Code Block |
---|
title | 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));
}
|
...
Code Block |
---|
title | 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());
}
|
...
Code Block |
---|
title | 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
|
...
Code Block |
---|
title | 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);
}
|
...
Code Block |
---|
title | 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);
}
|
...
Code Block |
---|
title | 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
}
|
...
Code Block |
---|
title | 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.
|
...
Code Block |
---|
title | 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
}
|
...
Code Block |
---|
title | 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.
|
...
Code Block |
---|
title | 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
}
|
...