Apr 14, 2011

Simple Integration test with JUnit Rules

Rules were introduced in JUnit 4.7 release. The short description is following:
The @Rule annotation allows you to annotate a public field in your test class, which is of type MethodRule. This binding will intercept test method calls like an AOP framework would do and redefine the execution, skip it, or do anything else.
Basically Rules is a way to avoid implementing of a custom test runner. In addition this approach gives more flexibility. You can implement your custom behaviour as a number of different rules and apply them to your test classes in different combinations.


My task was to create simple integration testing framework. Here are the most prominent requirements:
  • Generate and dynamically update configuration of the server (httpd)
  • Restart server when it is necessary
  • Provide exhaustive information in case of test failure (environment, server and modules configurations, request details, response details

I tried JUnit with custom rules to fulfill this assignment.It exceeded my expectations. Unfortunately it is no much information about JUnit rules applications. Thus I decided to share my experience. I hope it will be helpful.
Step 1
Create annotations for declarative server configuration.
ServerConfig.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServerConfig {
    ConfigDirective[] value();
}

ConfigDirective.java
public @interface ConfigDirective {
    Directive name();
    String value() default "";
    String[] values() default {};
}

Step 2
Create Rules Handler.

IntegrationRule.java
public class IntegrationRule implements MethodRule{
    private String vhost;
    private String requestUrl;
    private Map<String,String> requestCookies = new HashMap<String, String>();
    private TestResponse response;

    @Override
    public Statement apply(final Statement base, final FrameworkMethod method, final Object target) {
        final AbstractTest test = (AbstractTest)target;
        return new Statement() {

            @Override
            public void evaluate() throws Throwable {

                initContext();

                test.initConfig();

                // add type specific configuration
                ServerConfig tCfg = test.getClass().getAnnotation(ServerConfig.class);
                processAnnotation(tCfg, test);

                // add method specific configuration
                ServerConfig mCfg = method.getAnnotation(ServerConfig.class);
                processAnnotation(mCfg, test);

                test.updateConfig();

                try {
                    base.evaluate();
                } catch (Throwable e) {
                    dumpContext();
                    throw e;
                }
            }

            private void dumpContext() {
                String title = "--- " + target.getClass().getSimpleName() + "." + method.getName() + " ---";
                String delimiter = StringUtils.repeat("-", title.length());

                System.out.println(title);
                System.out.println(vhost);
                System.out.println(delimiter);
                System.out.println("Request URL: "+requestUrl);
                System.out.println(delimiter);
                if (!requestCookies.isEmpty()) {
                    System.out.println("Request cookies: "+requestCookies);
                    System.out.println(delimiter);
                }

                if (response != null) {
                    System.out.println(response);
                    System.out.println(delimiter);
                }
            }

        };
    }

    private void processAnnotation(ServerConfig cfg, AbstractTest test) {
        if (cfg != null) {
            ConfigDirective[] item = cfg.value();
            if (item != null) {
                for (ConfigDirective directive : item) {
                    Directive d = directive.name();
                    String v = directive.value();
                    List<String> vs = new ArrayList<String>(Arrays.asList(directive.values()));
                    if (StringUtils.isNotBlank(v)) {
                        vs.add(v);
                    }
                    for (String string : vs) {
                        test.getConfig().put(d, string);
                    }
                }
            }
        }
    }

    private void initContext() {
        requestCookies.clear();
        requestUrl = "";
        response = null;
        vhost = null;
    }

    public void putRequestCookies(String name, String value) {
        this.requestCookies.put(name, value);
    }

    public void setRequestUrl(String requestUrl) {
        this.requestUrl = requestUrl;
    }

    public void setResponse(TestResponse response) {
        this.response = response;
    }

    public void setVhost(String vhost) {
        this.vhost = vhost;
    }

}


Method "test.updateConfig()" generates new configuration, uploads it to the server and restarts httpd.

Step 3
Use rules in tests.

@ServerConfig({
    @ConfigDirective(
        name=Directive.ProtectedResource,
        value=SessionCookieExpirationTest.PATH+" "+SessionCookieExpirationTest.PRODUCT
    )
})
public class SessionCookieExpirationTest extends AbstractTest {
    protected static final String PATH = "/request-dump";
    protected static final String RESOURCE = "/resource.do";
    protected static final String PRODUCT = "TEST";
    protected static final String RESOURCE_URL = PATH + RESOURCE;

    @Test
    public void testExpiredSessionCookieDefaultNoChecking() {
        generateCookie(getString(Directive.SessionCookieName), true, TEST_USER_ID, PRODUCT);

        TestResponse response = doRequest(RESOURCE_URL);

        assertEquals(TestResponse.HTTP_CODE_OK, response.getStatusCode());
    }

    @Test
    @ServerConfig({
        @ConfigDirective(name=Directive.CookieExpires, value="1000")
    })
    public void testExpiredSessionCookieLogin() {

        generateCookie(getString(Directive.CookieName), true, TEST_USER_ID, PRODUCT);

        TestResponse response = doRequest(RESOURCE_URL);

        assertEquals(TestResponse.HTTP_CODE_REDIRECT, response.getStatusCode());

        String expectedRedirectUrl = getString(Directive.RedirectBaseUrl) +
            getString(Directive.LoginUri);

        String actualRedirectUrl = response.getRedirectUrl();
        assertEquals(expectedRedirectUrl, actualRedirectUrl);
    }
}

No comments: