Introduction
Unit test cases are an important and integral part of every application. Unit test cases help to ensure that the code works as expected. Any deviation from the expected behavior is caught before the source code reaches production environment. This article provides a quick introduction about using Hamcrest framework to write unit tests which are readable, maintainable.
Background
In the Java world, there have been many unit-testing frameworks, Junit and TestNG being the most popular. It's been a general trend that as and when the size and complexity of test cases grow, the unit test cases start becoming unreadable and in-expressive. This is where Hamcrest can help, write expressive unit test cases.
Core Features of Hamcrest
Improves Readability of Test Cases
The dialect/selection of method names in Hamcrest framework improves readability of test cases.
Following are some of the examples:
assertThat(actualValue, is(equalTo(expectedValue)));
assertThat(employee, hasProperty("firstName"));
assertThat(employee.getMaritalStatus(), isIn(new String[]{"Married", "Single - Never Married", "Widow", "Divorced"}));
Enforces Type Safety
Hamcrest method doesn't allow to compare apples with oranges, rather it enforces comparing apples with apples. This helps a lot while writing test cases as we won't be comparing the types that don't match. The first level of check is to ensure that we are comparing same objects. Hamcrest heavily uses Java generics to ensure type safety.
Some of the examples are as follows:
assertThat(employee.isManager(), is("true"));
Supports Chaining of Conditions
Hamcrest allows to write multiple conditions in single assertThat
statement. Multiple conditions can be grouped using logical conditions like or/and. We can write test cases where a condition can must match all the conditions or must match anyone of the condition. Some of the examples are as follows:
assertThat(employee.getFirstName(), allOf(notNullValue(), is("John")));
Supports Rich-set of Matchers
Hamcrest supports good number of Matchers already available, some of them are listed below:
Matcher Name | Description |
allOf | Creates a matcher that matches if examined object matches ALL of the specified matchers |
anyOf | Creates a matcher that matches if the examined object matches ANY of the specified matchers |
is | A shortcut to frequently used is(equalTo(x)) |
equalTo | Creates a matcher that matches when the examined object is logically equal to the specified operand, by calling the Object.equals |
not | Creates a matcher that wraps an existing matcher, but inverts the logic by which it will match |
notNullValue | Creates a matcher that matches if examined object is not null |
nullValue | Creates a matcher that matches if the examined object is null |
hasProperty | Creates a matcher that matches when the examined object has a JavaBean property with the specified name |
startsWith | Creates a matcher that matches if the examined String starts with the specified String |
endsWith | Creates a matcher that matches if the String ends with specified String |
Highly Extensible
Hamcrest not only provides its own rich feature set but it fully supports creation of custom Matchers. In order to write custom matcher, we need to extend from TypeSafeMatcher
base class.
Following is a simple example taken from Hamcrest documentation:
public class IsNotANumber extends TypeSafeMatcher<Double> {
@Override
public boolean matchesSafely(Double number) {
return number.isNaN();
}
public void describeTo(Description description) {
description.appendText("This is not a number");
}
@Factory
public static <T> Matcher<Double> notANumber() {
return new IsNotANumber();
}
}
The above created custom matcher can be used as:
assertThat(1.0, is(notANumber()));
As we can see, writing test cases using the Hamcrest constructs improves the readability of the test cases making them easy to understand and maintain.
Maven Dependency
The following Maven dependencies need to be added to pom.xml to use all the features of Hamcrest.
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
Sample Test Case
import com.test.matchers.Employee;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import org.junit.Before;
import org.junit.Test;
public class EmployeeTest {
private Employee employee;
@Before
public void initEmployee() {
employee = new Employee();
employee.setFirstName("John");
employee.setDesignation("Manager");
employee.setAge(30);
}
@Test
public void firstNameCannotBeNull() {
assertThat("", employee.getFirstName(), notNullValue());
}
@Test
public void firstNameEqualsJohn() {
assertThat("", employee.getFirstName(), is("John"));
}
@Test
public void designationShouldBeManagerOrContractor() {
assertThat(employee.getDesignation(), anyOf(is("Manager"), is("Contractor")));
}
@Test
public void employeeShouldBeOlderThan20() {
assertThat(employee.getAge(), greaterThan(20L));
}
@Test
public void employeeHasAgeProperty() {
assertThat(employee, hasProperty("age"));
}
}
References
History
- 29th September, 2014: Initial version