Sylvain Lemoine
Another dev blog in the nexus…

A custom Junit rule example

· by Sylvain Lemoine · Read in about 3 min · (617 Words)
Java Junit Tests

Confessions

I confess it, I had never written a Junit Rule. I use them, but never found any (real life project) reasonable purpose to write my own custom rule.

But after thinking twice about it lately I finally found one good reason to create one on my current business project !

Expected exception test

I’m used to applying the ExpectedException Rule to ensure proper exception instance class and exception message.

It works like this:

@RunWith(JUnit4.class)
public class AppTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Test
    public void testApp() {

        expectedException.expect(IllegalArgumentException.class);
        expectedException.expectMessage("my exception message");

        throw new IllegalArgumentException("my exception message");
    }
}

I usually use my own checked exception class to deal with functionnal exception cases:

public class BusinessException extends Exception {

    private Object[] parameters;

    public BusinessException(final String message, final Object... parameters) {
        super(message);
        this.parameters = parameters;
    }

    public Object[] getParameters() {
        return parameters;
    }
}

BusinessException is a straight-forward exception class made up of a message and any number of parameters.

Actually, the message is a key string like «user.not.found» which will, before outputting it to the client, be translated into the final message by using the Java ResourceBundle API (and usually its SpringFramework’s ResourceBundleMessageSource implementation). The process generally involved 0 or n parameters which will be included in the final message string.

For instance, if «user.not.found» relates to the message: «The user {0} cannot be found.», throwing a new BusinessException(«user.not.found», «Batman») will produce the message: «The user Batman cannot be found.».

Let’s write an expected exception test case for this:

@RunWith(JUnit4.class)
public class AppTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Test
    public void testApp() throws BusinessException {

        expectedException.expect(BusinessException.class);
        expectedException.expectMessage("user.not.found");

        throw new BusinessException("user.not.found", "Batman");
    }
}

It’s ok to check the expected message key but what about paremeters ?

A first attempt to check the parameter value might be:

@RunWith(JUnit4.class)
public class AppTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Test
    public void testApp() throws BusinessException {

        expectedException.expect(BusinessException.class);
        expectedException.expectMessage("user.not.found");

        try{
            throw new BusinessException("user.not.found", "Batman");
        }catch(BusinessException businessException){
            Assert.assertEquals(1,businessException.getParameters().length);
            Assert.assertEquals("Batman",businessException.getParameters()[0]);
            throw businessException;
        }
    }
}

Surely, it does the job, but it’s pretty cumbersome isn’t it.

besides you’ll probably copy paste it for every test for which you want to do an exception check or isolate it in super test class or in a public static method with some parameters.

Anyway, you can solve it much more nicely with a Junit Rule.

The rule


public class BusinessExceptionRule implements TestRule {

    private Object[] parameters;

    public void setParameters(Object... parameters) {
        this.parameters = parameters;
    }

    public Statement apply(Statement base, Description description) {
        this.parameters = new Object[0];
        return new BusinessExceptionStatement(base);
    }

    class BusinessExceptionStatement extends Statement {

        private final Statement base;

        public BusinessExceptionStatement(Statement base) {
            this.base = base;
        }

        @Override
        public void evaluate() throws Throwable {

            try {
                base.evaluate();
            } catch (Exception e) {
                if (e instanceof BusinessException) {
                    Object[] parameters = ((BusinessException) e).getParameters();
                    Assert.assertArrayEquals("Unexpected parameters for " + BusinessException.class,
                            BusinessExceptionRule.this.parameters, parameters);
                }

                throw e;

            }
        }
    }
}

BusinessExceptionRule is the rule public EntryPoint. The inner class BusinessExceptionStatement is a wrapper statement which will make the appropriate parameters checks by using the parameters set on the outer BusinessExceptionRule instance. Our base statement will be wrapped in try-catch block making the parameters assertion.

And thus all our tests can be rewritten into :

@RunWith(JUnit4.class)
public class AppTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Rule
    public BusinessExceptionRule businessExceptionRule = new BusinessExceptionRule();

    @Test
    public void willPass() throws BusinessException {

        expectedException.expect(BusinessException.class);
        expectedException.expectMessage("user.not.found");
        businessExceptionRule.setParameters("Batman");

        throw new BusinessException("user.not.found", "Batman");
    }

    @Test
    public void willPassWithNoParameters() throws BusinessException {
        expectedException.expect(BusinessException.class);
        expectedException.expectMessage("user.not.found");
        throw new BusinessException("user.not.found");
    }

    @Test
    public void willFail() throws BusinessException {

        expectedException.expect(BusinessException.class);
        expectedException.expectMessage("user.not.found");
        businessExceptionRule.setParameters("Batman", "Robin");
        throw new BusinessException("user.not.found", "Batman");
    }
}

which is a much more compact and less error-prone code !

Example sources are available at https://github.com/slem1/junit-rule-example

Comments