/ Java Programming


Cleanly handle exceptions in tests with Lambdas

If you have been writing tests for some time then the following construct should be pretty familiar to you for handling test cases:

@Test
public void myTest() {
 
    // Test something here... 

    try {
    
      // Test something that will throw SomeException

      Assert.fail("Test did not throw expected exception.");
    } catch (SomeException e) {
       // Expected  
    }

    // Test something here...
}

This has annoyed me some time already as it makes a clean looking test just plain ugly and I have been looking for a way to clean this mess up.

You might at this point now argue that you could split the test up in smaller blocks and use the @Test annotations support for assuming an exception is thrown like so:

@Test(expected=SomeException.class)
public void myTest() {
  /*
   * Run the test code here 
   */
}

And you are right, this would work for a simple case. But you will lose the capability to run test code after the exception has thrown.

Assume you are working on a bigger system and you need to test the fault tolerance of the system and check the state of the system after a exception has been thrown? For example if you have a system with database transaction, you want to make sure that the transaction is rolled back after the exception is thrown otherwise you will have your database in an inconsistent state after an exception is thrown. In this case you can't split the test up as that will not test the whole situation so you are again left with adding try-catch-statements in the middle of your test code making it hard to read.

With Java 8 lambdas and method references I've found we can clean up the test nicely, keeping the test clean and moving the try-catch far away for the test.

First we are going to need a utility method that handles the exception, I'll put that method in a utility class call TestingUtil.

/**
 * Testing utililies
 */
class TestingUtil {

    /**
     * Handles exceptions thrown from a runnable
     *
     * @param runnable 
     *      The runnable to run that will throw the exception
     * @param ex
     *      Expected exception
     * @param message
     *      Expected exception message. Can be null if message is not important.
     */
    public static void expectException(ThrowingRunnable runnable, Class<? extends Exception> ex, @Nullable String message) throws Exception {
        try {
            runnable.run();
            Assert.fail("Test did not throw expected " + ex.getSimpleName() + " exception.");
        } catch (Exception e) {
            if(e.getClass().isAssignableFrom(ex)){
                if(message != null){
                    Assert.assertEquals(e.getMessage(), message);
                }
            } else {
                throw e;
            }
        }
    }

    /**
     * Handles exceptions thrown from a runnable
     *
     * @param runnable 
     *      The runnable to run that will throw the exception
     * @param ex
     *      Expected exception
     */
    public static void expectException(ThrowingRunnable runnable, Class<? extends Exception> ex) throws Exception {
        TestingUtil.expectException(ThrowingRunnable runnable, Class<? extends Exception> ex, null);
    }
}

Next we are also going to need a an interface we can use as a lambda to wrap our code that will throw an exception. I'll call it ThrowingRunnable:

@FunctionalInterface
public interface ThrowingRunnable {
    void run() throws Exception;
}

Alright, we are not set to write our test:

@Test
public void myTest() throws Exception {
    
    // Test something here... 

    TestUtil.expectException(() -> {

        // Test something that will throw SomeException

    }, SomeException.class);

    // Test something here..

    TestUtil.expectException(() -> {

        // Test something that will throw EJBException 
        //when connection times out

    }, EJBException.class, "Transaction timed out");
   
    // Test that database is still in correct test
}

To me this is much more readable but not perfect. You can further clean this test up by adding some helper methods.

@Test
public void myTest() throws Exception {
    
    // Test something here... 

    expectSomeException(this::doDatabaseTransaction);

    // Test something here..

    expectTimeoutException(this::doDatabaseTransaction);

    // Test that database is still in correct state
}

private void doDatabaseTransaction() throws Exception{
    // Some code that will throw an exception and should rollback the state
}

private void expectSomeException(ThrowingRunnable runnable) {
    TestUtil.expectException(runnable, SomeException.class);
}

private  void expectTimeoutException(ThrowingRunnable runnable) {
    TestUtil.expectException(runnable, EJBException.class, "Transaction timed out");
}

And all of the sudden you again have very readable tests and can test that the state is ok both before and after the test without any extra boilerplate code.