Shaun Abram
Technology and Leadership Blog
Testing for expected exceptions in JUnit
Unit tests are used to verify that a piece of code operates as the developer expects it to. Sometimes, that means checking that the code throws expected exceptions too. JUnit is the standard for unit testing in Java and provides several mechanisms for verifying exceptions were thrown. This article explores the options and their relative merits.
(Note: There are other good options for testing for exceptions using AssertJ, rather than vanilla JUnit. See https://www.baeldung.com/assertj-exception-assertion)
Take the following simple code snippet as an example. As well as writing tests to ensure the canVote() method returns true or false, you should also writes tests to verify that it throws an IllegalArgumentException
when expected.
public class Student {
public boolean canVote(int age) {
if (i<=0) throw new IllegalArgumentException("age should be +ve");
if (i<18) return false;
else return true;
}
}
(Guava preconditions might be more suitable for these argument checks, but the example is still valid).
There are 3 common ways to check that exceptions are thrown, each with their own advantages and disadvantages.
1) @Test(expected…)
The @Test annotation has an optional parameter, “expected”, that allows you to specify a subclass of Throwable. If we wanted to verify that canVote()
() method above throws the correct exception, we would write:
@Test(expected = IllegalArgumentException.class)
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
student.canVote(0);
}
Simple and concise, if a little imprecise since it tests that the exception will be thrown somewhere in the method, rather than on a specific line.
2) ExpectedException
To use JUnit’s ExpectedException, first you need to declare the ExpectedException:
@Rule
public ExpectedException thrown = ExpectedException.none();
Then you can either use the simpler approach of specifying the expected exception only:
@Test
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
thrown.expect(NullPointerException.class);
student.canVote(0);
}
Or can also specify the expected exception message too:
@Test
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("age should be +ve");
student.canVote(0);
}
As well as being able to specify the expected exception message, this ExpectedException approach has the advantage of allowing you to be more precise about where the exception is expected to be thrown. In the above example, an unexpected IllegalArgumentException thrown in the constructor would cause the test to fail since we expected it to be thrown in the canVote() method.
On a side note, it would be nice if there wasn’t a need for the declaration:
@Rule public ExpectedException thrown= ExpectedException.none();
It just seems like unnecessary noise. It would be nice to just be able to do
expect(RuntimeException.class)
or
expect(RuntimeException.class, “Expected exception message”)
or at least be able to pass the exception and message in an single call to ExpectedException:
thrown.expect(IllegalArgumentException.class, “age should be +ve”);
3) Try/catch with assert/fail
Prior to JUnit4, the way to check for exceptions was to use try/catch blocks.
@Test
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
try {
student.canVote(0);
fail("expected IllegalArgumentException for non +ve age");
} catch (IllegalArgumentException ex) {
assertThat(ex.getMessage(), containsString("age should be +ve"));
}
}
Although an older approach, it is still perfectly valid. The main downside is that it is easy to forget to put the fail() after the catch, resulting in false positives if the expected exception is not thrown. I have certainly made this mistake in the past!
In conclusion, there are three main approaches for testing expected exceptions get thrown, each with its own advantages and disadvantages. For me personally, I usually lean towards the ExpectedException approach due to its precision and ability to test the exception message.
Tags: exceptions, JUnit, tdd, unittesting
Shaun,
thanks for the nice summary of exception testing techniques. There is a mistake in the try/catch example. The test will always fail because of the call to fail() at the end of the method. It should probably read:
try {
student.canVote(0);
fail( … );
} catch(IllegalArgumentException ex) {
…
}
– Rüdiger
Hi Rüdiger,
Thanks for the feedback, and for spotting the misplaced fail() call! Code updated now.
Thanks,
Shaun
all good and well but why not use catch-exception? https://code.google.com/p/catch-exception/
Hi Tomek,
Thanks for posting. I didn’t mention the Catch-Exception library simply because I hadn’t heard of it, so thanks for the link. I see from your web page that you have written a book on unit testing – I will check it out, but in the mean time, if you have any blog posts on Catch-Exception, please feel free to include them.
Thanks,
Shaun
Hi,
I included junit with maven:
junit
junit
4.11
test
I am trying your 2nd way which works fine with expect(Class) but not for expectMessage(String).
Do you have any ideas what went wrong?
I get the following error message:
java.lang.NoSuchMethodError: org.hamcrest.Matcher.describeMismatch(Ljava/lang/Object;Lorg/hamcrest/Description;)V
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:18)
at org.junit.Assert.assertThat(Assert.java:865)
at org.junit.Assert.assertThat(Assert.java:832)
at org.junit.rules.ExpectedException.handleException(ExpectedException.java:198)
at org.junit.rules.ExpectedException.access$500(ExpectedException.java:85)
at org.junit.rules.ExpectedException$ExpectedExceptionStatement.evaluate(ExpectedException.java:177)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
Thanks
Jens
Hi Jens,
Sounds like a classpath ordering, or maven dependency issue. Without seeing your maven pom, I would suggest trying to use the junit that excludes hamcrest, and then including hamcrest explicitly, so that you can control the exact version.
Also, see these postings on stackoverflow with similar issues:
http://stackoverflow.com/questions/7869711/getting-nosuchmethoderror-org-hamcrest-matcher-describemismatch-when-running
http://stackoverflow.com/questions/15833015/nosuchmethoderror-with-hamcrest-1-3-junit-4-11
It could be further complicated if you are using mockito too:
http://tedvinke.wordpress.com/2013/12/17/mixing-junit-hamcrest-and-mockito-explaining-nosuchmethoderror/
If you get a solution, feel free to post back here!
Shaun
HTTP response code to match.
Salute from the desk next to yours 🙂
Googled “junit test expected exception with message”, yours is second to stackoverflow.com only! Great post, thank you. Let me bask in the glow a little 🙂
So, the case I have can describe a more generic case than {{ExpectedException#expectMessage()}} does. In my case I want to make sure that org.springframework.web.client.HttpClientErrorException thrown within unit test of my Spring-Boot app is for HttpStatus.UNAUTHORIZED(401, “Unauthorized”) or FORBIDDEN(403, “Forbidden”). To achieve that my first guess was to use ExpectedException#expect(org.hamcrest.Matcher), something like:
public class SpringBootRestIntegrationTest {
RestTemplate rest;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() {
rest = new TestRestTemplate();
rest.setErrorHandler(new DefaultResponseErrorHandler());//resetting as TestRestTemplate’s constructor does silence its error handler
}
@Test
public void testACL4PUT() {
thrown.expect(new ExpectedHttpClientErrorExceptionMatcher(HttpStatus.UNAUTHORIZED));// <——– HERE's the trick
rest.put("http://localhost:8184/lcfs/testbucket?filename={filename}", "file content bla-bla".getBytes(), "test.txt");
}
}
HERE's the trick: I had to create my own org.hamcrest.Matcher (used by package org.junit.rules):
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;
public class ExpectedHttpClientErrorExceptionMatcher extends BaseMatcher {
private HttpStatus response_code;
public ExpectedHttpClientErrorExceptionMatcher(HttpStatus response_code) {
this.response_code = response_code;
}
@Override
public boolean matches(Object item) {
return item instanceof HttpClientErrorException && ((HttpClientErrorException)item).getStatusCode() == response_code;
}
@Override
public void describeTo(Description description) {
description.appendText(“HTTP response status “).appendValue(response_code);
}
}
As you can see the whole work boils down to implementing org.hamcrest.Matcher#matches(Object). Hope that helps 😉