Creating Custom Hamcrest Matchers
After we have finished this lesson, we
- Know how we can create custom Hamcrest matchers.
- Can create our Hamcrest matchers by using the best technique.
- Know when we should use custom Hamcrest matchers.
Creating Custom Hamcrest Matchers
When we want to create a custom Hamcrest matcher, we have to create a factory method that creates and configures our matcher. We can add this factory method into our test class, or create a so called object mother class and add our factory method into that class. We can choose the “correct” option by following these rules:
- If our matcher is used by only one test class, we should add this factory method into the test class.
- If our matcher is used by multiple test classes, we should create an object mother class and add our factory method into that class.
An object mother class is a class that contains static methods which create our custom Hamcrest matchers. We can create an object mother class by following these steps:
- Create a
publicandfinalclass. It is a good idea to mark the created class asfinalbecause it prevents developers from extending it. - Add a
privateconstructor to the created class. This prevents developers from creating instances of our object mother class.
The source code of our object mother class looks as follows:
1public final class TaskMatchers {
2
3 private TaskMatchers() {}
4}
When I name classes that create custom Hamcrest matchers, I like to append the string: ‘Matchers’ to the type of the matched object. Because our object mother class creates matchers which are used to write assertions for Task objects, its name should be: TaskMatchers.
Also, this example assumes that we need to use this object mother class from multiple test classes that are not necessarily in the same package as the object mother class. However, if this is not the case, we should limit the visibility of our object mother class.
We can now create custom Hamcrest matchers by using factory methods. Let’s move on and create a custom Hamcrest matcher that it used to verify that a single value is correct.
Creating a Custom Hamcrest Matcher for a Single Value
If we want to create a custom Hamcrest Matcher that is used to verify that a single value is correct, we should use the abstract FeatureMatcher<T, U> class. When we extend this class or create a new FeatureMatcher object, we have to provide two type parameters that are described in the following:
- The
Ttype parameter specifies the type of matched object. - The
Utype parameter specifies the type of the value (aka. feature of the matched object).
Let’s create a custom Hamcrest matcher which verifies that a Task object has the correct title. We can create this matcher by following these steps:
First, we have to add a new static factory method into the TaskMatchers class. This method returns a FeatureMatcher<Task, String> object and takes the expected title as a method parameter that is marked as final.
After we have added this factory method into our object mother class, its source code looks as follows:
1import org.hamcrest.FeatureMatcher;
2
3public final class TaskMatchers {
4
5 private TaskMatchers() {}
6
7 public static FeatureMatcher<Task, String> hasTitle(final String title) {
8 return null;
9 }
10}
Second, we have to modify our factory method to return a new FeatureMatcher<Task, String> object. When we create a new FeatureMatcher object, we have to provide the following constructor arguments:
- The Hamcrest matcher that is used to match the expected value with the actual value. Because we want to create a custom matcher which ensures that the actual title is equal to the expected title, we can create the correct Hamcrest matcher by invoking the
is()method of theMatchersclass. - The description that identifies the matched feature (e.g. expected value). We will pass the string: ’title’ as the second constructor argument.
- The mismatch message that is shown when our matcher fails. We will pass the string: ’title’ as the third constructor argument.
After we have created a new FeatureMatcher object, the source code of our object mother class looks as follows:
1import org.hamcrest.FeatureMatcher;
2
3import static org.hamcrest.Matchers.is;
4
5public final class TaskMatchers {
6
7 private TaskMatchers() {}
8
9 public static FeatureMatcher<Task, String> hasTitle(final String title) {
10 return new FeatureMatcher<Task, String>(is(title),
11 "title",
12 "title") {
13
14 //Does not compile yet
15 };
16 }
17}
Third, we have to override the featureValueOf(T actual) method of the FeatureMatcher<T, U> class. This method method must return the actual value of the matched property. That is why we must implement this method method by invoking the getTitle() method of the Task class.
After we have overridden this method, the source code of our object mother class looks as follows:
1import org.hamcrest.FeatureMatcher;
2
3import static org.hamcrest.Matchers.is;
4
5public final class TaskMatchers {
6
7 private TaskMatchers() {}
8
9 public static FeatureMatcher<Task, String> hasTitle(final String title) {
10 return new FeatureMatcher<Task, String>(is(title),
11 "title",
12 "title") {
13
14 @Override
15 protected String featureValueOf(Task actual) {
16 return actual.getTitle();
17 }
18 };
19 }
20}
That is all. We can now use our custom Hamcrest matcher in the same way as we use the “standard” Hamcrest matchers. The source code of the test method, which ensures that the title of the created task is set correctly, looks as follows:
1import org.junit.Test;
2
3import static org.junit.Assert.assertThat;
4
5public class TaskTest {
6
7 private static final Long ID = 1L;
8 private static final Long CREATOR_ID = 44L;
9 private static final String TITLE = "Write an example project";
10 private static final String DESCRIPTION = "description";
11
12 @Test
13 public void build_shouldCreateTaskWithCorrectTitle() {
14 Task task = Task.getBuilder()
15 .withId(ID)
16 .withCreator(CREATOR_ID)
17 .withTitle(TITLE)
18 .withDescription(DESCRIPTION)
19 .build();
20
21 assertThat(task, hasTitle(TITLE));
22 }
23}
If our test fails, we should see the following error message:
1java.lang.AssertionError:
2Expected: title is "Write an example project1"
3 but: title was "Write an example project"
Additional Reading:
Let’s move on and find out how we can create a custom Hamcrest matcher which verifies that multiple values are correct.
Creating a Custom Hamcrest Matcher for Multiple Values
If we have to create a custom Hamcrest matcher which verifies that multiple values are correct, we should use the TypeSafeDiagnosingMatcher<T> class. When we extend this class or create a new TypeSafeDiagnosingMatcher object, we have to provide one type parameter (T) that describes the type of the matched object.
Let’s create a custom Hamcrest matcher which verifies that a Task object is open. In other words, our matcher verifies that the status of the inspected task is TaskStatus.OPEN and the resolution is null. We can create this matcher by following these steps:
First, we have to add a new static factory method into the TaskMatchers class. This method returns a TypeSafeDiagnosingMatcher<Task> object and it takes no method parameters.
After we have added this factory method into our object mother class, its source code looks as follows:
1import org.hamcrest.TypeSafeDiagnosingMatcher;
2
3public final class TaskMatchers {
4
5 private TaskMatchers() {}
6
7 public static TypeSafeDiagnosingMatcher<Task> isOpen() {
8 return null;
9 }
10}
Second, we have to modify our factory method to return a new TypeSafeDiagnosingMatcher<Task> object. After we have created a new TypeSafeDiagnosingMatcher<Task> object, the source code of our object mother class looks as follows:
1import org.hamcrest.Description;
2import org.hamcrest.TypeSafeDiagnosingMatcher;
3
4public final class TaskMatchers {
5
6 private TaskMatchers() {}
7
8 public static TypeSafeDiagnosingMatcher<Task> isOpen() {
9 return new TypeSafeDiagnosingMatcher<Task>() {
10 //Does not compile
11 };
12 }
13}
Third, we have to override the describeTo() method. This method is used for building the first part of the error message that is shown when our matcher fails. This part should describe the expected values. That is why we will implement this method by explaining that the status of the task must be TaskStatus.OPEN and its resolution must be null.
After we overridden this method, the source code of our object mother class looks as follows:
1import org.hamcrest.Description;
2import org.hamcrest.TypeSafeDiagnosingMatcher;
3
4public final class TaskMatchers {
5
6 private TaskMatchers() {}
7
8 public static TypeSafeDiagnosingMatcher<Task> isOpen() {
9 return new TypeSafeDiagnosingMatcher<Task>() {
10
11 @Override
12 public void describeTo(Description description) {
13 description.appendText("An open task should have the status:")
14 .appendValue(TaskStatus.OPEN)
15 .appendText(" and resolution:")
16 .appendValue(null);
17 }
18 };
19 }
20}
Fourth, we have to override the matchesSafely() method of the TypeSafeDiagnosingMatcher<T> class. This method takes two method parameters which are described in the following:
- The matched object.
- The
Descriptionobject that is used to build the second part of the error message that is shown when our matcher returnsfalse. This error message should describe the actual values of the matched object.
We can implement this method by following these steps:
- Describe the actual values of the matched task. In other words, we have to create an error message that describes the actual values of the
statusandresolutionproperties. - Return
trueif the task is an open task andfalseotherwise.
After we have implement this method, the source code of our object mother class looks as follows:
1import org.hamcrest.Description;
2import org.hamcrest.TypeSafeDiagnosingMatcher;
3
4public final class TaskMatchers {
5
6 private TaskMatchers() {}
7
8 public static TypeSafeDiagnosingMatcher<Task> isOpen() {
9 return new TypeSafeDiagnosingMatcher<Task>() {
10
11 @Override
12 protected boolean matchesSafely(Task item,
13 Description mismatchDescription) {
14 mismatchDescription.appendText(" but had status:")
15 .appendValue(item.getStatus())
16 .appendText(" and resolution:")
17 .appendValue(item.getResolution());
18
19 return (item.getStatus() == TaskStatus.OPEN) &&
20 (item.getResolution() == null);
21 }
22
23 @Override
24 public void describeTo(Description description) {
25 description.appendText("An open task should have the status:")
26 .appendValue(TaskStatus.OPEN)
27 .appendText(" and resolution:")
28 .appendValue(null);
29 }
30 };
31 }
32}
We are done. We can now use our custom Hamcrest matcher in the same way as we use the “standard” Hamcrest matchers. The source code of the test method, which ensures that the created task is an open task, looks as follows:
1import org.junit.Test;
2
3import static org.junit.Assert.assertThat;
4
5public class TaskTest {
6
7 private static final Long ID = 1L;
8 private static final Long CREATOR_ID = 44L;
9 private static final String TITLE = "Write an example project";
10 private static final String DESCRIPTION = "description";
11
12 @Test
13 public void build_shouldCreateAnOpenTask() {
14 Task task = Task.getBuilder()
15 .withId(ID)
16 .withCreator(CREATOR_ID)
17 .withTitle(TITLE)
18 .withDescription(DESCRIPTION)
19 .build();
20
21 assertThat(task, isOpen());
22 }
23}
If our test fails, we should see the following error message:
1java.lang.AssertionError:
2Expected: An open task should have the status: <OPEN> and resolution: null
3 but: but had status: <CLOSED> and resolution: <DONE>
Additional Reading:
Let’s move on and find out how we should name our custom Hamcrest matchers.
How Should We Name Our Custom Hamcrest Matchers?
We should remember that the name of our factory method is also the “name” of our custom Hamcrest matcher. It is kind of obvious that we should give proper names to our factory methods, but this is easier said than done.
That is why I think that the following questions will help us when we are naming our factory methods:
- Does the matched property contain information that is simply shown and edited? If so, we should consider naming our factory method by prepending the string: ‘has’ to the name of the property (e.g.
hasTitle) - Does the matched property contain information about an event that happened in the past? If so, we should consider starting the name of our factory method with the string: ‘was’ (e.g.
wasCreatedBy). - Does that matched property (or properties) identify the state of the object? If so, we should consider starting the name of our factory 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 custom matcher, and sometimes we have to ignore the advice given on this section.
For example, if we want to create a custom matcher which ensures that a task was closed as a duplicate by the person X, we should consider using the name: wasClosedAsDuplicateBy. If we decide to do so, we chose 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 factory method with the string: ‘is’.
Let’s move on and find out when we should use custom Hamcrest matchers.
When Should We Use Custom Hamcrest Matchers?
Before we will create a custom Hamcrest matcher, we should always ask this question:
Will this matcher make our test code better? If so, how?
Because custom matchers add complexity into our test suite, we should use them only if their benefits outweigh their drawbacks. Let’s demonstrate this principle by comparing our custom Hamcrest matchers with assertions which are written by using the “standard” Hamcrest matchers.
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 Hamcrest matcher
2assertThat(task.getTitle(), is("title"));
3
4//Custom Hamcrest matcher
5assertThat(task, hasTitle("title"));
I think that in this case the extra effort is not worth it. The assertion which uses our custom Hamcrest matcher is a bit easier to read, but I think that the assertion, which uses the “standard” Hamcrest matcher, is fine as well.
Also, there is one more reason why we should not use custom Hamcrest matchers when we are writing data-centric assertions:
When do we stop? Should we write custom Hamcrest matchers for every property of every class? We must remember that writing custom Hamcrest matchers takes time, and we shouldn’t waste our time when our ROI is nonexistent.
In other words, we should not use custom Hamcrest matchers 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 Hamcrest matchers
2assertThat(task.getStatus(), is(TaskStatus.OPEN));
3assertThat("An open task cannot have a resolution",
4 task.getResolution(),
5 nullValue()
6);
7
8//Custom Hamcrest matcher
9assertThat(task, isOpen());
When we take a look at these assertions, we should notice immediately that here is extra effort is worth it because of two reasons:
- The assertion that uses our custom Hamcrest matcher is a lot easier to read than the assertions which use the “standard” Hamcrest matchers.
- Our custom Hamcrest matcher 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.
I think that this example has demonstrated that we should use custom Hamcrest matchers for removing business rules from our test methods.
The reason for this is that if we make changes to these business rules, and we use custom Hamcrest matchers, we have to make these changes only to our custom matchers. If we wouldn’t use custom matchers, we would have to make these changes to our test methods. This takes time and is very error-prone.
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 a custom Hamcrest matcher which ensures that a single value is correct, we should use the
FeatureMatcher<T, U>class. - If we want to create a custom Hamcrest matcher which ensures that multiple values are correct, we should use the
TypeSafeDiagnosingMatcher<T>class. - We should give proper names to the factory methods that create our custom Hamcrest matchers because these names are also the “names” or our custom matchers.
- We should use custom Hamcrest matchers for removing business rules from our test methods.