Writing Assertions for Exceptions

After we have finished this lesson, we:

  • Can write assertions for the type of the thrown exception.
  • Can write assertions for the thrown exception with the ExpectedException rule.
  • Can write assertions for the thrown exception with AssertJ.
  • Can select the best tool for the job.

Introduction to System Under Test

During this lesson we will write unit tests for the TaskFinder class. This class has one method called findById(). This method takes the id of the requested task as a method parameter and throws a NotFoundException.

The source code of the TaskFinder class looks as follows:

1public class TaskFinder {
2
3    public Task findById(Long id) {
4        throw new NotFoundException(id);
5    }
6}

The NotFoundException class extends the RuntimeException class, and it has a single constructor that constructs its error message and stores the id of the requested entity to its id field.

The source code of the NotFoundException class looks as follows:

 1public class NotFoundException extends RuntimeException {
 2
 3    private Long id;
 4
 5    NotFoundException(Long id) {
 6        super(String.format("No entity found with id: %d", id));
 7        this.id = id;
 8    }
 9
10    public Long getId() {
11        return id;
12    }
13}

Next we will write different assertions for the exception that is thrown by the findById() method. We will start by writing assertions for the type of the thrown exception.

Writing Assertions with the @Test Annotation

If we want to verify that our test passes only when the expected exception is thrown, we have to configure the type of the expected exception by setting the value of the @Test annotation’s expected attribute.

For example, if we want to verify that the findById() method of the TaskFinder class throws a NotFoundException, we have to follow these steps:

  1. Add a new test method into our test class.
  2. Implement the test method by invoking the findById() method of the TaskFinder class.
  3. Annotate the method with the @Test annotation and set the value of its expected attribute to NotFoundException.class.

The source code of our test class looks as follows:

 1import org.junit.Before;
 2import org.junit.Test;
 3
 4public class TaskFinderTest {
 5
 6    private static final Long ID = 1L;
 7
 8    private TaskFinder finder;
 9
10    @Before
11    public void createSystemUnderTest() {
12        finder = new TaskFinder();
13    }
14
15    @Test(expected = NotFoundException.class)
16    public void findById_ShouldThrowException() {
17        finder.findById(ID);
18    }
19}

This is a neat trick, but often we want to write other assertions for the thrown exception. Let’s move on and find out how we can write these assertions by using the ExpectedException rule.

Writing Assertions With the ExpectedException Rule

The ExpectedException rule gives us the possibility to catch the exception thrown by system under test and write assertions for the catched exception by using Hamcrest matchers.

If we want to use the ExpectedException rule, we have to enable it by following these steps:

  1. Add a public ExpectedException field into our test class and initialize it by invoking the static none() method of the ExpectedException class.
  2. Annotate the created field with the @Rule annotation.

After we have enabled the ExpectedException rule, the source code of our test class looks as follows:

 1import org.junit.Before;
 2import org.junit.Rule;
 3import org.junit.rules.ExpectedException;
 4
 5public class TaskFinderTest {
 6
 7    @Rule
 8    public ExpectedException thrown = ExpectedException.none();
 9
10    private static final Long ID = 1L;
11
12    private TaskFinder finder;
13
14    @Before
15    public void createSystemUnderTest() {
16        finder = new TaskFinder();
17    }
18}

We will now write two test methods which write different assertions for the exception thrown by the findById() method of the TaskFinder class.

First, if we want to verify that the findById() method throws a NotFoundException, we have to add a new test method into our test class and implement our test method by following these steps:

  1. Invoke the expect() method of the ExpectedException class and pass a Class object, which represents the expected exception, as a method parameter.
  2. Invoke the findById() method of the TaskFinder class.

The source code of our test method looks as follows:

 1import org.junit.Before;
 2import org.junit.Rule;
 3import org.junit.Test;
 4import org.junit.rules.ExpectedException;
 5
 6public class TaskFinderTest {
 7
 8    @Rule
 9    public ExpectedException thrown = ExpectedException.none();
10
11    private static final Long ID = 1L;
12
13    private TaskFinder finder;
14
15    @Before
16    public void createSystemUnderTest() {
17        finder = new TaskFinder();
18    }
19
20    @Test
21    public void findById_ShouldThrowException() {
22        thrown.expect(NotFoundException.class);
23        finder.findById(ID);
24    }
25}

Second, if we want to verify that the NotFoundException object contains the correct id, we have to add a new test method into our test class and implement it by following these steps:

  1. Invoke the expect() method of the ExpectedException class and pass a Hamcrest matcher, which verifies that the thrown NotFoundException object contains the correct id, as a method parameter.
  2. Invoke the findById() method of the TaskFinder class.

The source code of our test method looks as follows:

 1import org.junit.Before;
 2import org.junit.Rule;
 3import org.junit.Test;
 4import org.junit.rules.ExpectedException;
 5
 6public class TaskFinderTest {
 7
 8    @Rule
 9    public ExpectedException thrown = ExpectedException.none();
10
11    private static final Long ID = 1L;
12
13    private TaskFinder finder;
14
15    @Before
16    public void createSystemUnderTest() {
17        finder = new TaskFinder();
18    }
19
20    @Test
21    public void findById_ShouldThrowExceptionThatHasCorrectId() {
22        thrown.expect(hasProperty("id", is(ID)));
23        finder.findById(ID);
24    }
25}

Let’s move on and find out how we can write the same assertions with AssertJ.

Writing Assertions With AssertJ

If we are using Java 8, we can catch the thrown exception with AssertJ by invoking the static catchThrowable() method of the Assertions class. When we use this method, we have to know two things:

  • We have to pass a lambda expression that should throw an exception as a method parameter.
  • The catchThrowable() method returns the catched Throwable object or null if no exception was thrown.

After we have catched the thrown exception, we can write assertions for it by using the AssertJ API. We will now write two test methods which write different assertions for the exception thrown by the findById() method of the TaskFinder class.

First, if we want to verify that the findById() method throws a NotFoundException, we have to implement our test method by following these steps:

  1. Catch the Throwable object thrown by the findById() method.
  2. Write an assertion which verifies that the type of the thrown exception is NotFoundException.

The source code of our test method looks as follows:

 1import org.junit.Before;
 2import org.junit.Test;
 3
 4import static org.assertj.core.api.Assertions.assertThat;
 5import static org.assertj.core.api.Assertions.catchThrowable;
 6
 7public class TaskFinderTest {
 8
 9    private static final Long ID = 1L;
10
11    private TaskFinder finder;
12
13    @Before
14    public void createSystemUnderTest() {
15        finder = new TaskFinder();
16    }
17
18    @Test
19    public void findById_ShouldThrowException() {
20        Throwable thrown = catchThrowable(() -> finder.findById(ID));
21        assertThat(thrown).isExactlyInstanceOf(NotFoundException.class);
22    }
23}

Second, if we want to verify that the NotFoundException object contains the correct id, we have to write our test method by following these steps:

  1. Catch the Throwable object thrown by the findById() method.
  2. Cast the Throwable object to a NotFoundException object and put the casted object to a temporary variable.
  3. Write an assertion which verifies that the NotFoundException object contains the expected id.

The source code of our test method looks as follows:

 1import org.junit.Before;
 2import org.junit.Test;
 3
 4import static org.assertj.core.api.Assertions.assertThat;
 5import static org.assertj.core.api.Assertions.catchThrowable;
 6
 7public class TaskFinderTest {
 8
 9    private static final Long ID = 1L;
10
11    private TaskFinder finder;
12
13    @Before
14    public void createSystemUnderTest() {
15        finder = new TaskFinder();
16    }
17
18    @Test
19    public void findById_ShouldThrowExceptionThatHasCorrectId() {
20        Throwable thrown = catchThrowable(() -> finder.findById(ID));
21        NotFoundException notFound = (NotFoundException) thrown;
22
23        assertThat(notFound.getId()).isEqualByComparingTo(ID);
24    }
25}

Additional Reading:

We have now learned three techniques which we can use when we are writing assertions for the exceptions that are thrown by system under test. Let’s find out which technique we should use.

Which Technique Should We Use?

When we decide which technique we should use, we must compare our requirements with the strengths and weaknesses of each technique. Let’s go through these three techniques one by one and take a look at the strengths and weaknesses of each technique.

First, using the @Test annotation is an extremely simple and elegant way to to specify the type of the expected exception. However, it has two weaknesses:

  • We cannot write any other assertions for the thrown exception.
  • We cannot add any additional assertions into our test method or verify interactions between the system under test and our mock objects.

Second, the ExpectedException rule gives us the possibility to write versatile assertions for the exception that is thrown by system under test. That being said, it has three downsides:

  • It requires additional “setup” code. This means that if we are not careful, we might clutter the source code of our test class.
  • I am not big fan of the syntax that is supported by the ExpectedException rule because I have to specify the expected outcome before I can invoke the tested method.
  • We cannot add any additional assertions into our test method or verify interactions between the system under test and our mock objects.

Third, AssertJ actually catches the thrown exception. This means that we can add additional assertions into our test method, verify interactions between the system under test and our mock objects, and write assertions for the thrown exception by using the fluent AssertJ API. However, AssertJ has two drawbacks:

  • If we want to catch exceptions with AssertJ, we have to use Java 8.
  • We cannot use AssertJ without adding the AssertJ dependency into our classpath. This can be problem if we are working with people who don’t allow us to use it.

I think that we should choose the used technique by following these rules:

  • If we have to write an assertion, which verifies that the type of the thrown exception is correct, we should write our assertion with the @Test annotation.
  • If we are not using Java 8 or we cannot use AssertJ, we should write our assertions by using the ExpectedException rule.
  • If we are using Java 8 and we can use AssertJ, we should write our assertions with AssertJ.
  • If we want to add additional assertions into our test method or verify interactions between the system under test and our mock objects, we have to write our assertions with AssertJ.

It’s time to summarize what we learned from this lesson.

Summary

This lesson has taught us three things:

  • If we have to write an assertion, which verifies that the type of the thrown exception is correct, we should write our assertion with the @Test annotation.
  • If we want to write other assertions for the thrown exception, we should use either the ExpectedException rule or AssertJ.
  • If we want to add additional assertions into our test method or verify interactions with the system under test and our mock objects, we must write our assertions with AssertJ.

Get the source code from Github