This tutorial explains software testing using the Hamcrest matcher framework.
1. Using Hamcrest matcher for unit tests
1.1. Purpose of the Hamcrest matcher framework
Hamcrest is a widely used framework for unit testing in the Java world.
Hamcrest target is to make your tests easier to write and read. For this, it provides additional matcher classes which can be used in test for example written with JUnit. You can also define custom matcher implementations.
To use Hamcrest matchers in your test you use the assertThat
statement followed by one or several matchers.
The usage of Hamcrest matchers is demonstrated by the following code snippet.
package com.vogella.junit5.hamcrest;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.isA;
import org.junit.jupiter.api.Test;
class HamcrestInstanceOfTest {
@Test
void demosInstanceOfTest() {
assertThat(Long.valueOf(1), instanceOf(Integer.class));
// shortcut for instanceOf
assertThat(Long.valueOf(1), isA(Integer.class));
}
}
Hamcrest is typically viewed as a third generation matcher framework.
The first generation used For example, the
|
It is also possible to chain matchers, via the anyOf
or allOf
method.
assertThat("test", anyOf(is("testing"), containsString("est")));
Hamcrest tries also to create readable error messages.
assertTrue(result instanceof String);
// error message:
java.lang.AssertionError
at org.junit.Assert.fail(Assert.java:86)
at org.junit.Assert.assertTrue(Assert.java:41)
at org.junit.Assert.assertTrue(Assert.java:52)
// ...
assertEquals(String.class, result.getClass());
// error message:
java.lang.NullPointerException
at com.vogella.hamcrest.HamcrestTest.test(HamcrestTest.java:30)
// ....
assertThat(result, instanceOf(String.class));
// error message:
java.lang.AssertionError:
Expected: an instance of java.lang.String
but: null
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:8)
// ...
2. Making the Hamcrest library available
To use Hamcrest in your Maven or Gradle project for you tests you have to add the dependencies to the test dependencies. You find the latest Hamcrest version on Maven central via https://search.maven.org/artifact/org.hamcrest/hamcrest.
The following contains the settings for Gradle and Maven.
Defining the Hamcrest dependency for Gradle
To use Hamcrest matchers for a project based on the Gradle build system, add the following dependencies to it.
dependencies {
// more dependencies, e.g. for JUnit...
testImplementation 'org.hamcrest:hamcrest:2.2'
}
Defining the Hamcrest dependency for Maven
Add the following dependency to your pom file to use Hamcrest for your tests in a Maven based project.
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
3. Using Hamcrest
3.1. Static import
To make all matchers available in your file add a static import. This also makes it easier to find matchers through code completion. You can add them to your test or configure your IDE to present them to you. You will later find an exercise for the Eclipse IDE.
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
3.2. Hamcrest matchers for lists
The usage of the Hamcrest matchers for lists are demonstrated by the following example.
com.vogella.unittest.hamcrest;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.core.Every.everyItem;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
class HamcrestListMatcherExampleTest {
@Test
void listShouldInitiallyBeEmpty() {
List<Integer> list = Arrays.asList(5, 2, 4);
assertThat(list, hasSize(3));
// ensure the order is correct
assertThat(list, contains(5, 2, 4));
assertThat(list, containsInAnyOrder(2, 4, 5));
assertThat(list, everyItem(greaterThan(1)));
}
}
// Check that a list of objects has a property race and
// that the value is not ORC
assertThat(fellowship, everyItem(hasProperty("race", is(not((ORC))))));
3.3. Overview of Hamcrest matcher
The following are the most important Hamcrest matchers:
-
allOf
- matches if all matchers match (short circuits) -
anyOf
- matches if any matchers match (short circuits) -
not
- matches if the wrapped matcher doesn’t match and vice -
equalTo
- test object equality using the equals method -
is
- decorator for equalTo to improve readability -
hasToString
- test Object.toString -
instanceOf
,isCompatibleType
- test type -
notNullValue
,nullValue
- test for null -
sameInstance
- test object identity -
hasEntry
,hasKey
,hasValue
- test a map contains an entry, key or value -
hasItem
,hasItems
- test a collection contains elements -
hasItemInArray
- test an array contains an element -
hasProperty
- checks if a Java Bean has a certain property can also check the value of this property -
closeTo
- test floating point values are close to a given value -
greaterThan
,greaterThanOrEqualTo
,lessThan
,lessThanOrEqualTo
-
equalToIgnoringCase
- test string equality ignoring case -
equalToIgnoringWhiteSpace
- test string equality ignoring differences in runs of whitespace -
containsString
,endsWith
,startsWith
- test string matching
To see all matchers, use API reference.
4. Exercise: Using Hamcrest matchers
Create or re-use a Maven or Gradle project named com.vogella.unittest
.
4.1. Making the Hamcrest library available
To use Hamcrest in your Maven or Gradle project for you tests you have to add the dependencies to the test dependencies. You find the latest Hamcrest version on Maven central via https://search.maven.org/artifact/org.hamcrest/hamcrest.
The following contains the settings for Gradle and Maven.
Defining the Hamcrest dependency for Gradle
To use Hamcrest matchers for a project based on the Gradle build system, add the following dependencies to it.
dependencies {
// more dependencies, e.g. for JUnit...
testImplementation 'org.hamcrest:hamcrest:2.2'
}
Defining the Hamcrest dependency for Maven
Add the following dependency to your pom file to use Hamcrest for your tests in a Maven based project.
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
4.2. Configuring Eclipse to use the Hamcrest static imports
The Eclipse IDE cannot always create the corresponding static import
statements automatically.
You can configure the Eclipse IDE to use code completion to insert typical JUnit method calls and to add the static import automatically. For this open the corresponding setting via
.Use the New Type button to add the following entries to it:
-
org.hamcrest.MatcherAssert
-
org.hamcrest.Matchers
4.3. Using Hamcrests built-in matchers in unit tests
The target of this exercise is to make yourself familiar with Hamcrest matchers. The tests are trivial but you will get familiar with the usage of the Hamcrest matchers.
Navigate into the org.hamcrest.Matchers class to see available matchers. |
Create the following new test class called HamcrestSimpleExamplesTests
.
package com.vogella.unittest.hamcrest;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
public class HamcrestSimpleExamplesTests {
// tests for the list
@DisplayName("Tests for the List")
@Nested
class ListTests {
private List<Integer> list;
@BeforeEach
public void setup() {
list = Arrays.asList(5, 2, 4);
}
@Test
@DisplayName("List should have an intial size of 3")
void ensureInitialSize() {
fail();
}
@Test
@DisplayName("Check content of the array")
void containsNumbersInAnyOrder() {
fail();
} private Integer[] ints;
@Test
void everyItemGreaterThan1() {
fail();
}
}
@DisplayName("Tests for the array")
@Nested
class ArrayTests {
private Integer[] ints;
@BeforeEach
public void setup() {
ints = new Integer[] { 7, 5, 12, 16 };
}
@Test
void arrayHasSizeOf4() {
fail();
}
@Test
void arrayContainsNumbersInGivenOrder() {
fail();
}
}
@DisplayName("Tests for the Task")
@Nested
class TaskTests {
// class to be tested
public class Task {
private final long id;
private String summary;
private String description;
private int year;
public Task(long id, String summary, String description) {
this.id = id;
this.summary = summary;
this.description = description;
}
// getters/setters
}
// tests for the Task
@Test
void objectHasSummaryProperty() {
Task task = new Task(1, "Learn Hamcrest", "Important");
fail();
}
@Test
void objectHasCorrectSummaryValue() {
Task task = new Task(1, "Learn Hamcrest", "Important");
fail();
}
@Test
void objectHasSameProperties() {
Task todo1 = new Task(1, "Learn Hamcrest", "Important");
Task todo2 = new Task(1, "Learn Hamcrest", "Important");
fail();
}
}
@DisplayName("Tests for String")
@Nested
class StringTests {
// tests for string
@Test
void ensureThatAnEmptryStringIsEmpty() {
String input = "";
fail();
}
@Test
void ensureThatAStringIsEitherNullOrEmpty() {
String input = "";
fail();
}
}
}
4.4. Using Hamcrest collection matchers for lists
Implement the tests for the list.
Ensure via individual tests with Hamcrest matchers that list
:
-
has a size of 3
-
contains the elements 2, 4, 5 in any order
-
every item is greater than 1
Show Solution
package com.vogella.unittest.hamcrest;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
// more
// tests for the list
@DisplayName("Tests for the List")
@Nested
class ListTests {
private List<Integer> list;
@BeforeEach
public void setup() {
list = Arrays.asList(5, 2, 4);
}
@Test
@DisplayName("List should have an intial size of 3")
void ensureInitialSize() {
assertThat(list, hasSize(3));
}
@Test
@DisplayName("Check content of the array")
void containsNumbersInAnyOrder() {
assertThat(list, containsInAnyOrder(2, 4, 5));
}
@Test
void everyItemGreaterThan1() {
assertThat(list, everyItem(greaterThan(1)));
}
}
4.4.1. Using Hamcrest collection matchers for arrays
Ensure via tests with Hamcrest matchers that the ints
array
-
has a size of 4
-
contains 7, 5, 12, 16 in the given order
Show Solution
package com.vogella.unittest.hamcrest;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.arrayWithSize;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
//more
@DisplayName("Tests for the array")
@Nested
class ArrayTests {
private Integer[] ints;
@BeforeEach
public void setup() {
ints = new Integer[] { 7, 5, 12, 16 };
}
@Test
void arrayHasSizeOf4() {
assertThat(ints, arrayWithSize(4));
}
@Test
void arrayContainsNumbersInGivenOrder() {
assertThat(ints, arrayContaining(7, 5, 12, 16));
}
}
4.4.2. Using Hamcrest beans matchers
Review the Task
class, which will be used in your test.
Write tests that ensure that:
-
Task has a property called "summary"
-
If Task is constructed with the summary "Learn Hamcrest" that the summary property was initialized with this value
-
Two objects created with the same values, have the same property values
Show Solution
This test requires that you have generated / created getter and setter for the Task
class.
In Eclipse which can be done via .
package com.vogella.unittest.hamcrest;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.samePropertyValuesAs;
import org.junit.jupiter.api.Test;
@DisplayName("Tests for the Task")
@Nested
class TaskTests {
// class to be tested
public class Task {
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public long getId() {
return id;
}
private final long id;
private String summary;
private String description;
private int year;
public Task(long id, String summary, String description) {
this.id = id;
this.summary = summary;
this.description = description;
}
// getters/setters
}
//more
@Test
void objectHasSummaryProperty() {
Task task = new Task(1, "Learn Hamcrest", "Important");
assertThat(task, hasProperty("summary"));
}
@Test
void objectHasCorrectSummaryValue() {
Task task = new Task(1, "Learn Hamcrest", "Important");
assertThat(task, hasProperty("summary", equalTo("Learn Hamcrest")));
}
@Test
void objectHasSameProperties() {
Task todo1 = new Task(1, "Learn Hamcrest", "Important");
Task todo2 = new Task(1, "Learn Hamcrest", "Important");
assertThat(todo1, samePropertyValuesAs(todo2));
}
}
4.4.3. Using Hamcrest String matchers
Write tests that ensure that:
-
"" is an empty string
-
a given string is either empty or null
Show Solution
package com.vogella.unittest.hamcrest;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.emptyOrNullString;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
@DisplayName("Tests for String")
@Nested
class StringTests {
@Test
void ensureThatAnEmptryStringIsEmpty() {
String input = "";
assertThat(input, is(emptyString()));
}
@Test
void ensureThatAStringIsEitherNullOrEmpty() {
String input = "";
assertThat(input, is(emptyOrNullString()));
}
}
5. Exercise - Writing a custom Hamcrest matcher using FeatureMatcher
The target of this exercise is to write a custom matcher with Hamcrest.
5.1. Create Hamcrest Matchers
Define a custom matcher for Hamcrest which provides the length
matcher for a String.
We want to use the class FeatureMatcher
.
With FeatureMatcher we can wrap an existing Matcher, decide which field of the given Object under test to match
and provide a nice error message.
The constructor of FeatureMatcher takes the following arguments in this order:
-
The matcher we want to wrap
-
a description of the feature that we tested
-
a description of the possible mismatch
The only method we have to overwrite is featureValueOf(T actual)
which returns the value which will get passed into the wrapped matches()
/matchesSafely()
method.
public static Matcher<String> length(Matcher<? super Integer> matcher) {
return new FeatureMatcher<String, Integer>(matcher, "a String of length that", "length") {
@Override
protected Integer featureValueOf(String actual) {
return actual.length();
}
};
}
5.2. Validate
Use your custom matcher in a test to check if the String "Gandalf" has a length of 8. Adjust the test if necessary.
5.3. Solutions for FeatureMatcher
Show Solution
@Test
public void fellowShipOfTheRingShouldContainer7() {
assertThat("Gandalf", length(is(7)));
}
public static Matcher<String> length(Matcher<? super Integer> matcher) {
return new FeatureMatcher<String, Integer>(matcher, "a String of length that", "length") {
@Override
protected Integer featureValueOf(String actual) {
return actual.length();
}
};
}
6. Exercise: Writing your custom Hamcrest matcher using TypeSafeMatcher
It is possible to write your custom Hamcrest matcher by extending TypeSafeMatcher.
In contrast to BaseMatcher the TypeSafeMatcher class automatically checks for null
values, checks the type and casts appropriately before delegating to matchesSafely()
.
It provides type safety by default.
The following is an example for defining a matcher which allows testing if a String matches a regular expression.
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
public class RegexMatcher extends TypeSafeMatcher<String> {
private final String regex;
public RegexMatcher(final String regex) {
this.regex = regex;
}
@Override
public void describeTo(final Description description) {
description.appendText("matches regular expression=`" + regex + "`");
}
@Override
public boolean matchesSafely(final String string) {
return string.matches(regex);
}
// matcher method you can call on this matcher class
public static RegexMatcher matchesRegex(final String regex) {
return new RegexMatcher(regex);
}
}
The following snippet gives an example how to use it.
package com.vogella.android.testing.applicationtest;
import org.junit.Test;
import static org.hamcrest.MatcherAssert.assertThat;
public class TestCustomMatcher {
@Test
public void testRegularExpressionMatcher() throws Exception {
String s ="aaabbbaaaa";
assertThat(s, RegexMatcher.matchesRegex("a*b*a*"));
}
}
7. Exercise: Combining matchers
Combining matchers is supported by Hamcrest out of the box but it has the limitation that the error is hard to read:
@Test
public void () {
List<Integer> list = new ArrayList<>();
assertThat(list, both(hasSize(1)).and(contains(42)));
}
Expected: (a collection with size <1> and iterable containing [<42>])
but: a collection with size <1> collection size was <0>.
This not very readable.
7.1. Target
We want to write our own MatcherCombiner that provides us with a readable error message, even when multiple matchers fail.
7.2. Create MatchCombiner
We do this by inheriting from BaseMatch and by providing a starting method that let’s us chain matchers together. The matchers get saved in a list that we iterate over during the matching phase.
public class MatcherCombinator<T> extends BaseMatcher<T> {
private final List<Matcher<? super T>> matchers = new ArrayList<>();
private final List<Matcher<? super T>> failedMatchers = new ArrayList<>();
private MatcherCombinator(final Matcher<? super T> matcher) {
matchers.add(matcher);
}
public MatcherCombinator<T> and(final Matcher<? super T> matcher) {
matchers.add(matcher);
return this;
}
@Override
public boolean matches(final Object item) {
boolean matchesAllMatchers = true;
for (final Matcher<? super T> matcher : matchers) {
if (!matcher.matches(item)) {
failedMatchers.add(matcher);
matchesAllMatchers = false;
}
}
return matchesAllMatchers;
}
@Override
public void describeTo(final Description description) {
description.appendValueList("\n", " " + "and" + "\n", "", matchers);
}
@Override
public void describeMismatch(final Object item, final Description description) {
description.appendText("\n");
for (Iterator<Matcher<? super T>> iterator = failedMatchers.iterator(); iterator.hasNext();) {
final Matcher<? super T> matcher = iterator.next();
description.appendText("Expected: <");
description.appendDescriptionOf(matcher).appendText(" but ");
matcher.describeMismatch(item, description);
if (iterator.hasNext()) {
description.appendText(">\n");
}
}
}
public static <LHS> MatcherCombinator<LHS> matches(final Matcher<? super LHS> matcher) {
return new MatcherCombinator<LHS>(matcher);
}
}
To validate the implementation we write a new test.
@Test
public void test() {
List<Integer> list = new ArrayList<>();
assertThat(list, matches(hasSize(1)).and(contains(42)));
}
java.lang.AssertionError:
Expected:
<a collection with size <1>> and
<iterable containing [<42>]>
but:
Expected: <a collection with size <1> but collection size was <0>>
Expected: <iterable containing [<42>] but No item matched: <42>.
You can adjust this output in the describeMismatch
method.
8. Grouping your matchers for import
If you define many custom matchers it might become tedious to import them one by one into your test files. By grouping them into a single class you can import them with one statement. You can also group them together with Hamcrest matchers.
package com.vogella.hamcrest;
import com.vogella.hamcrest.matchers.RegexMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
public class MyMatchers
{
public static <T> Matcher<T> instanceOf(Class<T> target) {
return Matchers.instanceOf(target);
}
public static Matcher<String> matchesRegex(String target) {
return RegexMatcher.matchesRegex(target);
}
}
In your test file:
import static com.vogella.hamcrest.MyMatchers.*;
9. Hamcrest assert statements compared to pure JUnit5 assert statements
The following snippets compare pure JUnit assert statements with Hamcrest matchers.
Junit5 | Hamcrest |
---|---|
[source, java] ---- assertEquals(expected, actual); ---- |
[source, java] ---- assertThat(actual, is(equalTo(expected))); ---- |
assertNotEquals(expected, actual) |
assertThat(actual, is(not(equalTo(expected)))); |
10. Hamcrest resources
include::../10_Include/sourcejava.adoc[] If you need more assistance we offer https://learn.vogella.com/[Online Training] and https://www.vogella.com/training/[Onsite training] as well as https://www.vogella.com/consulting/[consulting]
11. Conclusion
You have learned to use the Hamcrest API to simplify writing and reading your unit tests.
The full implementation of all these examples and code snippets can be found in my Hamcrest project. The Hamcrest Maven examples are located in Hamcrest with Maven github project and the Gradle Hamcrest examples are located in Hamcrest with Gradle github project.