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
ExpectedExceptionrule. - 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:
- Add a new test method into our test class.
- Implement the test method by invoking the
findById()method of theTaskFinderclass. - Annotate the method with the
@Testannotation and set the value of itsexpectedattribute toNotFoundException.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}
Additional Reading:
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:
- Add a
publicExpectedExceptionfield into our test class and initialize it by invoking thestaticnone()method of theExpectedExceptionclass. - Annotate the created field with the
@Ruleannotation.
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}
Additional Reading:
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:
- Invoke the
expect()method of theExpectedExceptionclass and pass aClassobject, which represents the expected exception, as a method parameter. - Invoke the
findById()method of theTaskFinderclass.
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:
- Invoke the
expect()method of theExpectedExceptionclass and pass a Hamcrest matcher, which verifies that the thrownNotFoundExceptionobject contains the correct id, as a method parameter. - Invoke the
findById()method of theTaskFinderclass.
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 catchedThrowableobject ornullif 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:
- Catch the
Throwableobject thrown by thefindById()method. - 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:
- Catch the
Throwableobject thrown by thefindById()method. - Cast the
Throwableobject to aNotFoundExceptionobject and put the casted object to a temporary variable. - Write an assertion which verifies that the
NotFoundExceptionobject 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
ExpectedExceptionrule 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
@Testannotation. - If we are not using Java 8 or we cannot use AssertJ, we should write our assertions by using the
ExpectedExceptionrule. - 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.
Additional Reading:
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
@Testannotation. - If we want to write other assertions for the thrown exception, we should use either the
ExpectedExceptionrule 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.