Creating Custom Assertions With AssertJ

After we have finished this lesson, we

  • Know how we can create custom assertions with AssertJ.
  • Understand when we should use custom assertions.

Soft Assertions Don’t Necessarily Help Us to Write Clean Tests

In the previous lesson we wrote a test that uses soft assertions and ensures that the created Task object is an open task. The source code of this test looks as follows:

 1import org.assertj.core.api.SoftAssertions;
 2import org.junit.Test;
 3
 4import static org.assertj.core.api.Assertions.assertThat;
 5
 6public class TaskTest {
 7
 8    private static final Long ID = 1L;
 9    private static final Long ASSIGNEE_ID = 99L;
10    private static final Long CREATOR_ID = 44L;
11    private static final String TITLE = "Write an example project";
12    private static final String DESCRIPTION = "description";
13
14    @Test
15    public void build_shouldCreateAnOpenTaskSoft() {
16        Task task = Task.getBuilder()
17                .withId(ID)
18                .withAssignee(ASSIGNEE_ID)
19                .withCreator(CREATOR_ID)
20                .withTitle(TITLE)
21                .withDescription(DESCRIPTION)
22                .build();
23
24        SoftAssertions assertions = new SoftAssertions();
25
26        assertions.assertThat(task.getStatus())
27                .overridingErrorMessage(
28                        "The status of an open task must be: %s but was: %s",
29                        TaskStatus.OPEN,
30                        task.getStatus()
31                )
32                .isEqualTo(TaskStatus.OPEN);
33
34        assertions.assertThat(task.getResolution())
35                .overridingErrorMessage(
36                        "An open task cannot have a resolution but had: %s",
37                        task.getResolution()
38                )
39                .isNull();
40
41        assertions.assertAll();
42    }
43}

As we remember , this test method has three unresolved problems:

  • It adds business logic into our test suite. This is a bad idea. If we make changes to this logic, we have to make the same changes to all tests which are testing the changed logic. This is a maintenance nightmare.
  • It is not as easy to read as tests which have only one assertion. I think that each new assertion makes a test method harder to read. To make matters worse, typically real-life applications have complex states that require “too many” assertions. If we would add these assertions into our test methods, our tests could look quite cryptic for people who see them at the first time.
  • It contains technical implementation details. This is a bad idea because this can shift the reader’s attention from the essence of our test to the technical implementation details.

Let’s find out how we can solve these problems.

Custom Assertions to the Rescue

We can solve our problem by creating a so called custom assertion class and moving our assertions from our test method into our custom assertion class. We can create custom assertions with AssertJ by following these steps:

First, we have to create a public and final class. It is a good idea to mark our assertion class as final because it prevents developers from extending it.

The source code of our custom assertion class looks as follows:

1public final class TaskAssert {
2
3}

When I name my custom assertion classes, I like to append the string: ‘Assert’ to the type of the asserted object. Because our assertion class is used to write assertions for Task objects, its name should be: TaskAssert.

Also, this example assumes that we need to use this assertion class from multiple test classes that are not necessarily in the same package as the assertion class. However, if this is not the case, we should limit the visibility of our assertion class.

Second, we have to modify our custom assertion class to extend the AbstractAssert<S extends AbstractAssert<S,A>, A> class. When we extend the AbstractAssert class, we have to provide two type parameters:

  1. The type of our custom assertion class (TaskAssert).
  2. The type of the asserted object (Task).

After we have extended the AbstractAssert class, the source code of the TaskAssert class looks as follows:

1import org.assertj.core.api.AbstractAssert;
2
3public final class TaskAssert extends AbstractAssert<TaskAssert, Task> {
4
5}

Third, we have to add a private constructor to the TaskAssert class. We can create this constructor by following these steps:

  1. Ensure that the constructor takes the actual Task object as a constructor argument.
  2. Invoke the constructor of the AbstractAssert class and pass the following objects as constructor arguments:
  3. The actual object.
  4. a Class object that represents our custom assertion class (TaskAssert.class).

After we have added this constructor into the TaskAssert class, its source code looks as follows:

1import org.assertj.core.api.AbstractAssert;
2
3public final class TaskAssert extends AbstractAssert<TaskAssert, Task> {
4
5    private TaskAssert(Task actual) {
6        super(actual, TaskAssert.class);
7    }
8}

Fourth, we have to add a new static factory method into the TaskAssert class. This method returns a TaskAssert object and takes the actual Task object as a method parameter.

After we have added this factory method into the TaskAssert class, its source code looks as follows:

 1import org.assertj.core.api.AbstractAssert;
 2
 3public final class TaskAssert extends AbstractAssert<TaskAssert, Task> {
 4
 5    private TaskAssert(Task actual) {
 6        super(actual, TaskAssert.class);
 7    }
 8
 9    public static TaskAssert assertThatTask(Task actual) {
10        return new TaskAssert(actual);
11    }
12}
When I name my these factory methods, I like to append the type of the asserted object to the string: ‘assertThat’. Because our assertion class is used to write assertions for Task objects, the name of our factory method should be: assertThatTask().

Fifth, we have to add a custom assertion method into the TaskAssert class. We can do this by following these steps:

  1. Add a public isOpen() method into the TaskAssert class. This method takes no method parameters, and it returns a reference to the TaskAssert object.
  2. Verify that the actual Task object is an open task by using the SoftAssertions class.
  3. Return a reference to the used TaskAssert object. This gives us the possibility to chain our custom assertions.

After we have added the isOpen() method to our custom assertion class, its source code looks as follows:

 1import org.assertj.core.api.AbstractAssert;
 2import org.assertj.core.api.SoftAssertions;
 3
 4public final class TaskAssert extends AbstractAssert<TaskAssert, Task> {
 5
 6    private TaskAssert(Task actual) {
 7        super(actual, TaskAssert.class);
 8    }
 9
10    public static TaskAssert assertThatTask(Task actual) {
11        return new TaskAssert(actual);
12    }
13
14    public TaskAssert isOpen() {
15        SoftAssertions assertions = new SoftAssertions();
16
17        assertions.assertThat(actual.getStatus())
18                .overridingErrorMessage(
19						"The status of an open task must be: %s but was: %s",
20                        TaskStatus.OPEN,
21                        actual.getStatus()
22                )
23                .isEqualTo(TaskStatus.OPEN);
24
25        assertions.assertThat(actual.getResolution())
26                .overridingErrorMessage(
27						"An open task cannot have a resolution but had: %s",
28                        actual.getResolution()
29                )
30                .isNull();
31
32        assertions.assertAll();
33
34        return this;
35    }
36}

We are done. After we have modified our test method to use our new custom assertion, the source code of our test class looks as follows:

 1import org.junit.Test;
 2
 3public class TaskTest {
 4
 5    private static final Long ID = 1L;
 6    private static final Long ASSIGNEE_ID = 99L;
 7    private static final Long CREATOR_ID = 44L;
 8    private static final String TITLE = "Write an example project";
 9    private static final String DESCRIPTION = "description";
10
11    @Test
12    public void build_shouldCreateAnOpenTaskSoft() {
13        Task task = Task.getBuilder()
14                .withId(ID)
15                .withAssignee(ASSIGNEE_ID)
16                .withCreator(CREATOR_ID)
17                .withTitle(TITLE)
18                .withDescription(DESCRIPTION)
19                .build();
20
21        assertThatTask(task).isOpen();
22	}
23}

Let’s move on and find out how we should name our custom assertions.

How Should We Name Our Custom Assertions?

This section contains the same advice as the section that help us to name our custom Hamcrest matchers .

We should remember that the name of our custom assertion method is also the “name” of our custom assertion. It is kind of obvious that we should give proper names to assertion methods, but this is easier said than done.

That is why I think that the following questions will help us to get started:

  • Does the asserted property contain information that is simply shown and edited? If so, we should consider naming our assertion method by prepending the string: ‘has’ to the name of the property (e.g. hasTitle)
  • Does the asserted property contain information about an event that happened in the past? If so, we should consider starting the name of our assertion method with the string: ‘was’ (e.g. wasCreatedBy).
  • Does that asserted property (or properties) identify the state of the object? If so, we should consider starting the name of our assertion method with the string ‘is’ (e.g. isInProgress).

Also, it is important to remember that these questions are not rules. Their only purpose is to help us to figure out the best possible name for our assertion method, and sometimes we have to ignore the advice given on this section.

For example, if we want to create a custom assertion which ensures that a task was closed as a duplicate by the person X, we should consider using the name: wasClosedWithResolutionDuplicateBy. If we decide to do so, we choose to ignore the advice which states that if the matched property identifies the state of the object, we should consider starting the name of our assertion method with the string: ‘is’.

Let’s move on and find out when we should use custom assertions.

When Should We Use Custom Assertions?

This section contains the same advice as the section that describes when we should use custom Hamcrest matchers . However, this section has some AssertJ specific advice as well.

Before we create a custom assertion, we should always ask this question:

Will this assertion make our test code better? If so, how?

Because custom assertions add complexity into our test suite, we should use them only if their benefits outweigh their drawbacks. Let’s demonstrate this principle by comparing a few custom assertions with assertions which are written by using the “standard” AssertJ assertions.

First, if we have to write an assertion which ensures that the title of a task is correct, we can use one of these two assertions:

1//Standard AssertJ assertion
2assertThat(task.getTitle()).isEqualTo("title")
3
4//Custom AssertJ assertion
5assertThatTask(task).hasTitle("title");

I think that in this case the extra effort might not be worth it. The custom assertion is a bit easier to read, but I think that the assertion, which uses the “standard” AssertJ API, is fine as well.

Also, there is one more reason why we should not use custom AssertJ assertions when we are writing data-centric assertions:

When do we stop? Should we write custom assertion for every property of every class? We must remember that writing custom assertions takes time, and we shouldn’t waste our time when our ROI is nonexistent.

In other words, we should not use custom assertions if we are writing data-centric assertions. That being said, there are some exceptions to this rule. If we want highlight specific properties (like an address) or create a domain-specific language, we should use custom assertions even if we are writing data-centric assertions.

Second, if we have to ensure that a task is an open task, we can use one of these two assertions:

 1//Standard AssertJ assertion
 2SoftAssertions assertions = new SoftAssertions();
 3
 4assertions.assertThat(actual.getStatus())
 5		.overridingErrorMessage(
 6				"The status of an open task must be: %s but was: %s",
 7				TaskStatus.OPEN,
 8				actual.getStatus()
 9		)
10		.isEqualTo(TaskStatus.OPEN);
11
12assertions.assertThat(actual.getResolution())
13		.overridingErrorMessage(
14				"An open task cannot have a resolution but had: %s",
15				actual.getResolution()
16		)
17		.isNull();
18
19assertions.assertAll();
20
21//Custom AssertJ assertion
22assertThatTask(task).isOpen();

When we take a look at these assertions, we should notice immediately that here is extra effort is worth it because of three reasons:

  • The assertion that uses our custom assertion is a lot easier to read than the assertions which use the “standard” AssertJ assertions.
  • Our custom assertion helps us to remove business rules from our test method. This is a good thing because it makes our tests easier to write and maintain.
  • Our custom assertion removes technical implementation details from our test method. This makes our tests easier to read.

I think that this example has demonstrated that we should use custom assertions for removing business rules and technical implementation details from our test methods because of two reasons:

  • If we make changes to our business rules, and we use custom assertions, we have to make these changes only to our custom assertions. If we wouldn’t use custom assertions, we would have to make these changes to our test methods. This takes time and is very error-prone.
  • Technical implementation details make our tests hard to read because they shift reader’s attention from the essence of our test case to the technical implementation details.

Let’s move on and summarize what we learned from this lesson.

Summary

This lesson has taught us four things:

  • If we want to create assertions with AssertJ, we have to extend the AbstractAssert class.
  • We should give proper names to our assertion methods because these names are also the “names” or our custom assertions.
  • If we want highlight specific properties (like an address) or create a domain-specific language, we should use custom assertions even if we are writing data-centric assertions.
  • We should use custom assertions for removing business rules and technical implementation details from our test methods.

Get the source code from Github .