JUnit’s ExpectedException @Rule

Some time ago I had written a post about different ways to deal with exceptions using JUnit. It’s time to augment that post with a truly great way to achieve the same result.

Let’s look at this through an example. The task will be to implement a little method for an insurance company that registers a car to be insured by its license plate number. In Hungary, license plates follow the LLL-DDD pattern, where L stands for an uppercase letter, while D is a digit from 0 to 9. The initial requirement is simple: validate whether the input string conforms to the pattern described above; if it does, save it, else, throw an IllegalArgument exception.

Nothing easier, we quickly come up with something like this (to keep it short, we’ll deal with the license checking part only):

public class LicenseValidator {

	private static final Pattern LICENSE_PLATE_PATTERN = Pattern.compile("[A-Z]{3}-[0-9]{3}");

	public void checkLicense(String license) {
		Matcher matcher = LICENSE_PLATE_PATTERN.matcher(license);
		if (!matcher.matches()) {
			throw new IllegalArgumentException();
		}
	}
}

And a test for this:

	@Test(expected=IllegalArgumentException.class)
	public void shouldTrowExceptionWhenLicenseIsMalformed() {
		LicenseValidator validator = new LicenseValidator();

		validator.checkLicense("13-JHN");
	}

Ok, this was easy, but now the next requirement comes along. From a license plate, you can infer the age of a car; the earlier the letters come in the alphabet, the older the car (AAA-001 is the oldest possible). Due to different company policies, the insurer does not want to bother with old cars; all the ones with a license number from the range AAA-001 to DZZ-999 are disallowed for any insurance plan. When the application encounters such an old license number, it should -again- throw an IllegalArgumentException. The texts of the exceptions should now tell what the reason of denial was.

Ok, so how about this:

public class LicenseValidator {

	private static final Pattern LICENSE_PLATE_PATTERN = Pattern.compile("[A-Z]{3}-[0-9]{3}");

	public void checkLicense(String license) {
		Matcher matcher = LICENSE_PLATE_PATTERN.matcher(license);
		if (!matcher.matches()) {
			throw new IllegalArgumentException("Malformed license number");
		}

		if (license.charAt(0)<'E') {
			throw new IllegalArgumentException("Vehicle too old to be insured");
		}
	}

}

With these test cases:

	@Test(expected=IllegalArgumentException.class)
	public void shouldTrowExceptionWhenLicenseIsMalformed() {
		LicenseValidator validator = new LicenseValidator();

		validator.checkLicense("13-JHN");
	}

	@Test(expected=IllegalArgumentException.class)
	public void shouldTrowExceptionWhenVehicleTooOld() {
		LicenseValidator validator = new LicenseValidator();

		validator.checkLicense("BAA-123");
	}

From the point of view of functionality, this is ok. The only problem is that it is easy to make a mistake within the second test. Maybe we forget a letter or a digit; the test will still pass (since we’ve got a malformed license), but a great load of the code will not be covered with unit tests.

We can always fall back to the g’old try-catch-getMessage() trio, but that would result in some extremely messy test cases. There is a better way: utilizing JUnit’s @Rule annotation in conjunction with the ExpectedException class.

Here’s how we can solve the issue elegantly:

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

	@Test
	public void shouldTrowExceptionWhenLicenseIsMalformed() {
		LicenseValidator validator =new LicenseValidator();

		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("Malformed license number");

		validator.checkLicense("13-JHN");

	}

	@Test
	public void shouldTrowExceptionWhenVehicleTooOld() {
		LicenseValidator validator =new LicenseValidator();

		expectedException.expect(IllegalArgumentException.class);
		expectedException.expectMessage("Vehicle too old to be insured");

		validator.checkLicense("BLC-576");
	}

Now if we supplied a malformed license number for the second test, we’d get a red bar, as the expected message would not match the actual one.

A couple things to notice here:

  • expectedException is public. That’s not a typo – JUnit requires our expected exception @Rule declared public. Failing to do so will result in an ugly exception: java.lang.Exception: The @Rule ‘expectedException’ must be public.
  • @Rule does not behave like @Mock in case of Mockito. Despite the annotation, we still had to initialize the expectedException field using the ExpectedException.none() static factory ethod
  • ExpectedException is capable of expecting a certain exception type, an exception message or both.
  • The expectations have to proceed the method call that throws the exception
  • In order to expect no exception, one simply does not set up any expectations in the unit test

Also, it is good to know that expectMessage can also be used with matchers for greater flexibility.

When to use it? The ExpectedException is only preferable to @Test(expected=…) when the exception’s message has to be assertable. If the exception’s class is enough to appear in the assertions, use the latter one. That’s because -as pointed out earlier- rules have to be public – which is not a very good object oriented design, and also using the expected exception rues there’s more work to do. @Test(expected=…) is more straightforward, easier to understand, so use that construct when possible.

Advertisements

Author: tamasgyorfi

Senior software engineer, certified enterprise architect and certified Scrum master. Feel free to connect on Twitter: @tamasgyorfi

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s