Testing for expected exceptions
Unit testing – we’re all well versed at it by now, we know why we do it and we know how to do it. It’s baked into our development lifecycle and we do it well. Right? …
This post looks at the case where we are expecting a given test scenario to throw an exception, and how we describe that in a test. I’ll be using java and JUnit as my examples, but the theory and concepts should be applicable to any language and test framework.
JUnit’s annotations
JUnit has a series of annotations and optional arguments that are used to configure a test. In particular it has an `expected` argument which is used to tell the test runner that you are expecting an exception of a given type. Consider the following code (ignoring my bad practice of if’s on one line !!):
public void someMethod(String someArg) { if (someArg == null) throw new IllegalArgumentException("arg cannot be null"); if (someArg.length() == 0) throw new IllegalArgumentException("arg must not be empty string"); // do something with someArg }
Then consider the following 2 tests:
@Test(expected = IllegalArgumentException.class) public void shouldFailGivenNull() { someclass.someMethod(null); } @Test(expected = IllegalArgumentException.class) public void shouldFailGivenEmptyString() { someclass.someMethod(""); }
What threw the exception?
See the problem? We can’t be 100% sure why or where the exception was thrown. We are not being explicit about what should throw the exception. All we are saying and asserting is that something should throw the exception and we are not making any assertions as to the reason of the exception.
We also have a problem if something in the test code before our method call throws the same exception that we have said we are expecting:
@Test(expected = IllegalArgumentException.class) public void shouldFailGivenEmptyString() { // Given HashMap someMap = new HashMap(-1); // When someclass.someMethod(""); }
OK, so I’m not sure why you would create a map with a negative capacity, but it serves to demonstrate the problem. It will throw an IllegalArgumentException, which will apparently pass the test, even though someMethod was never invoked with the empty string!
The solution
So how do we do this better? A better pattern is to explicitly use a try/catch:
@Test public void shouldFailGivenNull() { // Given String methodArg = null; // When try { someclass.someMethod(methodArg); fail("Was expecting an IllegalArgumentException); } // Then catch (IllegalArgumentException e) { assertThat(e.getMessage(), is("arg cannot be null"); } }
Whilst the test is longer and requires more typing, this is a much better pattern as we’re being completely explicit about where the exception should be thrown, and we are making assertions about the reason.