Unit Testing Struts2 Actions With Annotation-based Validation

Unit testing Struts actions became a lot easier in Struts 2. With actions being POJOs rather than having to extend a base Action class, unit tests are incredibly simple. Throw in mocking any dependencies using something like Mockito and you have a class that can be unit tested in complete isolation…with one caveat, validation.

While the easiest way to implement validation is to extend the ActionSupport class, you can implement the Validateable and ValidationAware interfaces yourself. Validating the action normally requires you to somehow implement a little bit of the container. This can be done easily enough but it breaks the idea of unit tests just being about the class and starts to wander into the territory of integration tests. Not a big problem really but it can be a real pain in the arse when you have a large project with several hundred tests that all require the initialisation and tear-down of the Struts2 stack.

In Struts 2.0.x, there was a way round this. The class ActionValidatorManagerFactory was provided in the source code that allowed you to create an instance of the ActionValidatorManager. You could use this to call validate on the execute method and you were done.

In Struts 2.1.x, for reasons unknown, the ActionValidatorManager class has been removed. So how are you supposed to validate your actions now? The answer is to re-implement it yourself.

So here is our example action. Note that it’s super simple and has just one field to validate.

public class SimpleAnnotatedAction extends ActionSupport {

    private String name;

    public String execute() {
        return Action.SUCCESS;
    }

    public String getName() {
        return name;
    }

    @RequiredStringValidator(message = "Please specify a simple name")
    public void setName(String name) {
        this.name = name;
    }
}

Here is our Unit test for our action

public class SimpleAnnotatedActionTest {

    private ActionValidatorManager avm;

    @Before
    public void setup() throws ClassNotFoundException {
        avm = ActionValidationManagerFactory.create(
                  ActionValidationManagerFactory.Type.DEFAULT);
    }

    @Test
    public void shouldTestSimpleAnnotationBasedValidation() throws ValidationException {
        SimpleAnnotatedAction action = new SimpleAnnotatedAction();

        avm.validate(action, "");
        assertTrue(action.getFieldErrors().size() > 0);
        assertEquals("Please specify a simple name",
                      action.getFieldErrors().get("name").get(0));
    }
}

And here’s the magic bit, our home grown ActionValidationManagerFactory.  We have to instantiate a container object because of Struts2’s reliance on auto-wiring.

public class ActionValidationManagerFactory {

    public enum Type {
        DEFAULT("default"), NO_ANNOTATION("no-annotations");

        private final String value;

        private Type(String value) {
            this.value = value;
        }
    }

    public static ActionValidatorManager create(Type type) throws ClassNotFoundException {
        Container container = createContainer();

        ActionContext context = new ActionContext(new HashMap<String, Object>());
        context.setValueStack(createValueStack(container));
        ActionContext.setContext(context);

        return container.getInstance(ActionValidatorManager.class, type.value);
    }

    private static ValueStack createValueStack(Container container)
          throws ClassNotFoundException {
        return container.getInstance(ValueStackFactory.class).createValueStack();
    }

    private static Container createContainer() {
        ContainerBuilder builder = new ContainerBuilder();
        builder.factory(ObjectFactory.class, Scope.SINGLETON)
                .factory(ActionProxyFactory.class,
                    DefaultActionProxyFactory.class, Scope.SINGLETON)
                .factory(ObjectTypeDeterminer.class,
                    DefaultObjectTypeDeterminer.class, Scope.SINGLETON)
                .factory(XWorkConverter.class, Scope.SINGLETON)
                .factory(ValueStackFactory.class,
                    OgnlValueStackFactory.class, Scope.SINGLETON)
                .factory(ValidatorFactory.class,
                    DefaultValidatorFactory.class, Scope.SINGLETON)
                .factory(ValidatorFileParser.class,
                    DefaultValidatorFileParser.class, Scope.SINGLETON)
                .factory(PatternMatcher.class,
                    WildcardHelper.class, Scope.SINGLETON)
                .factory(ReflectionProvider.class,
                    OgnlReflectionProvider.class, Scope.SINGLETON)
                .factory(ReflectionContextFactory.class,
                    OgnlReflectionContextFactory.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, CompoundRoot.class.getName(),
                    CompoundRootAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, Object.class.getName(),
                    ObjectAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, Iterator.class.getName(),
                    XWorkIteratorPropertyAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, Enumeration.class.getName(),
                    XWorkEnumerationAccessor.class, Scope.SINGLETON)
                .factory(Configuration.class,
                    DefaultConfiguration.class, Scope.SINGLETON)
                .factory(UnknownHandlerManager.class,
                    DefaultUnknownHandlerManager.class, Scope.SINGLETON)

                 // silly workarounds for ognl since there is no way to flush its caches
                .factory(PropertyAccessor.class, List.class.getName(),
                    XWorkListPropertyAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, ArrayList.class.getName(),
                    XWorkListPropertyAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, HashSet.class.getName(),
                    XWorkCollectionPropertyAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, Set.class.getName(),
                    XWorkCollectionPropertyAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, HashMap.class.getName(),
                    XWorkMapPropertyAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, Map.class.getName(),
                    XWorkMapPropertyAccessor.class, Scope.SINGLETON)

                .factory(PropertyAccessor.class, Collection.class.getName(),
                    XWorkCollectionPropertyAccessor.class, Scope.SINGLETON)
                .factory(PropertyAccessor.class, ObjectProxy.class.getName(),
                    ObjectProxyPropertyAccessor.class, Scope.SINGLETON)
                .factory(MethodAccessor.class, Object.class.getName(),
                    XWorkMethodAccessor.class, Scope.SINGLETON)
                .factory(MethodAccessor.class, CompoundRoot.class.getName(),
                    CompoundRootAccessor.class, Scope.SINGLETON)
                .factory(NullHandler.class, Object.class.getName(),
                    InstantiatingNullHandler.class, Scope.SINGLETON)
                .factory(ActionValidatorManager.class,
                    AnnotationActionValidatorManager.class, Scope.SINGLETON)
                .factory(ActionValidatorManager.class, "no-annotations",
                    DefaultActionValidatorManager.class, Scope.SINGLETON)
                .factory(TextProvider.class, "system",
                    DefaultTextProvider.class, Scope.SINGLETON)
                .factory(OgnlUtil.class, Scope.SINGLETON)
                .factory(XWorkBasicConverter.class, Scope.SINGLETON);
        builder.constant("devMode", "false");
        return builder.create(true);
    }
}

2 thoughts on “Unit Testing Struts2 Actions With Annotation-based Validation

  1. Jens

    Hi Martin,

    thx for your post – exactly what I was looking for 🙂

    Unfortunately I get a

    java.lang.NullPointerException at com.opensymphony.xwork2.validator.AnnotationActionValidatorManager.buildValidatorKey(AnnotationActionValidatorManager.java:201)

    when testing with struts 2.1.8.1 – what version did you use?

    Regards, Jens

  2. martin

    I used Struts 2.1.6. I will have a look at version 2.1.8.1 to see what exactly in the xwork-core jar that is causing this.

    Thanks,
    Martin

Comments are closed.