
Mommy's Little Test Developer
Continuing with my look at the new core rules in JUnit 4.7 this blog entry will look at the Verifier core rule. This rule is the base class for rules like the ErrorCollector rule which I looked at in a previous post. Using the Verifier core rule it is possible to turn tests which would otherwise pass into failing tests based on a verification state.
There is a single method in the Verifier class that implementors of this rule need to override. The verify method is declared with a throwable exception which should be thrown to indicate verification failure. Here is the method declaration from the Verifier class:
/**
* Override this to add verification logic.
* Overrides should throw an exception to indicate that verification failed.
*/
protected void verify() throws Throwable {}
When the Verifier rule is applied the following apply method is used.
public Statement apply(final Statement base, FrameworkMethod method,
Object target) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
verify();
}
};
}
As can be seen once the base statement evaluation is completed the overriden verify method will be run. This enables the implementation class to fail tests which would otherwise pass.
So how could this be employed in a useful manner? Under what circumstances would it make sense to fail a passing test based on an object state ending up in an incorrect state. John Smart recently wrote an article about Testing Exceptions in JUnit 4.7 in which he implements the verify method in order to check a user is not logged in.
John states that his implementation is probably not the intended use of the Verifier rule, but I don’t think he was far from an intended use. John did provide me with a use case for the Verifier rule though. So my implementation will be used to verify that a user is still in the session once the test has run.
All John failed to implement in his example is a version of the verify method that throws an exception. Following the same pattern as the ErrorCollector rule for my UserInSessionVerifier class that extends Verifier I implemented a verify method that throws a NoUserInSessionException. Below is my UserInSessionVerifier class code:
import javax.servlet.http.HttpSession;
import org.junit.rules.Verifier;
public class UserInSessionVerifier extends Verifier {
HttpSession session;
public UserInSessionVerifier(HttpSession session) {
this.session = session;
}
@Override
protected void verify() throws Throwable {
NoUserInSessionException.assertNotEmpty(session);
}
}
As can be seen this is a fairly simple class which extends the Verifier rule class and overrides the verify method. This overriden method throws a NoUserInSessionException if the session user state verification fails, that happens if the session does not contain a user. The session property is initialized in the constructor, which is then passed by parameter to the assertNotEmpty static method of the NoUserInSessionException class. Lets take a look at that class now:
import javax.servlet.http.HttpSession
public class NoUserInSessionException extends Exception {
private static final long serialVersionUID = 1L;
private final HttpSession session;
public NoUserInSessionException(HttpSession session) {
this.session = session;
}
public HttpSession getSession() {
return this.session;
}
public static void assertNotEmpty(HttpSession session) throws Throwable {
if (session.getAttribute("userName") == null
|| session.getAttribute("userName").equals("")) {
throw new NoUserInSessionException(session);
}
}
}
This standard exception class extends a Throwable subclass, it also implements the static assertNotEmpty method that contains the user state verification code. In this trivial example if the userName attribute in the session is empty or null a NoUserInSessionException is thrown – as the user state is not what we expected it to be. If the user state is as we expect – that is to say it is populated then this method will not throw an exception and the verify method in UserInSessionVerifier will not cause any tests to fail.
Having looked at our Verifier rule (UserInSessionVerifier) and its associated exception (NoUserInSessionException), lets now look at a test that uses our new UserInSessionVerifier rule.
import static org.junit.Assert.assertTrue;
import javax.servlet.http.HttpSession;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.mock.web.MockHttpSession;
public class UserInSessionVerifierExample {
public HttpSession session = new MockHttpSession();
@Rule
public UserInSessionVerifier verifyUserInSession = new UserInSessionVerifier(session);
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void userInSessionPasses() {
// Add user to the session.
session.setAttribute("userName", "testUser");
assertTrue(true);
}
@Test
public void userNotInSessionFails() {
// No user added to session
assertTrue(true);
}
// This fails
// - Because the exception is not returned by base.evaluate() but
// by the verify() call in the Verifier class apply() method.
@Test(expected = NoUserInSessionException.class)
public void userNotInSessionUncaught() {
// No user added to session
assertTrue(true);
}
@Test
public void userNotInSessionCaught() {
// No user added to session
thrown.expect(NoUserInSessionException.class);
assertTrue(true);
}
@After
public void removeUserFromSession() {
if (session.getAttribute("userName") != null) {
session.removeAttribute("userName");
}
}
}
Looking at this class you will notice that there are 2 JUnit 4.7 rules, 4 tests and an @After annotated method in it. Lets look closer at these aspects of the test class now.
The first rule declared is the UserInSessionVerifier rule that we implemented. We pass this rule the MockHttpSession as an argument to the constructor. This rule will be evaluated after each test is run, so if there is no user in the session at the end of a test the rule will fail verification. The second rule used is an ExpectedException rule which will be used to test that when the verification fails we actually recieve a NoUserInSessionException.
The 4 tests all contain a simple passing assertTrue(true) statement which means they should all run successfully. The difference comes in the verification state once the tests complete. The userInSessionPasses test places a user into the session, thus when the verification rule is applied the state is verified and the test passes successfully. The userNotInSessionFails test does not place a user in the session and although the base statement passes the test fails due to the verification state.
The other 2 tests show how the NoUserInSessionException thrown by the UserInSessionVerifier method gets handled. The userNotInSessionUncaught method shows that using the expected exception attribute to catch the NoUserInSessionException fails. This fails as the base evaluate method does not throw the exception – which is what the expected exception attribute checks. The exception is thrown by the verfiy method. Using the ExpectedException rule which catches exceptions even during the verification state we can catch this exception. The userNotInSessionCaught test passes as the ExpectedException rules expect method rule is met – the NoUserInSessionException is thrown.
The @After method simply clears the user from the session. As can seen this runs after the verification rule, otherwise the userInSessionPasses test would fail. I included it to test when the verification rule gets fired. I wanted to check that the rule was being fired before any clean up methods.
I belive that John could update his verifier to check if a user was logged in based on the example presented here. I would be interested to know if he thinks such an implementation would prove to be scalable or not. Shall follow up with him.
Hopefully this has provided you with some ideas on how to use the Verifier rule. Would be great to hear about implementations of it you create and how it gets used. I shall move on to investigate the TestWatchman rule in my next installment, so hopefully you will be back for more!



