http://www.developer.com/

Back to article

Write More Understandable Java Tests with Matcher Objects and FEST-Assert


August 30, 2010

When writing Java unit tests, developers often face the problem of their unit test assertions being long and hard to read. For example, consider the following JUnit test:


private List words;
@Before
public void setUp() {
words = Arrays.asList("available", "avenge", "avenue", "average");
}

@Test
public void shouldContainAllElementsStartingWithavAndEndingWithe() {
for (String word : words) {
assertTrue(word.startsWith("av") && word.endsWith("e"));
}
}

This JUnit test case has a setUp method (annotated with the @Before annotation) that will initialize some data for the test, and then it has a test method. To test that all the elements in the collection start with av and end with e, the code first iterates over the loop and then uses two assertions (starting with av and ending with e). This test method also includes some boilerplate code, which iterates over a loop and then fires assertions on each element.

This code is rather hard to read and understand, isn't it? Wouldn't it be great if you could do all this testing in one line of code? In this article, I will demonstrate how to use a library of matchers for building test expressions. The library is called Hamcrest and it contains lots of helpful matcher objects. It helps you specify matching rules which you can use for unit testing.

 

Benefits of Using Matchers in Java Tests

Let’s rewrite the above JUnit test using Hamcrest library.


@Test
public void shouldContainAllElementsStartingWithavAndEndingWithe() {
assertThat(words, everyItem(both(startsWith("av")).and(endsWith("e"))));
}

In this test method, we use the assertThat method with the Hamcrest matchers nested within each other. Nesting is one of Hamcrest’s powerful features because it allows you to build complex assertions. The assertThat method has the following signature:


public static void assertThat(T actual, Matcher matcher)

It asserts that actual satisfies the condition specified by matcher and will throw an AssertionError if the actual does not satisfy the condition.

As you can see, the test case written using Hamcrest is much more readable as it communicates its intention very clearly. It can be read as follows: assert that every word in the words collection should start with "av" and end with "e. " As you start using matchers, you will learn how easy it is to write assertions that are human readable and easy to understand. They will just become a part of your unit testing toolkit.

The two main benefits of using matchers in your unit tests are:

  1. Clean and readable code: If you have followed the discussion you understand that with the help of matchers your test code can be as easy to read as simple English.
  2. Descriptive failure message: If the test fails, the failure trace is very descriptive, which helps in debugging the problem.

Let’s modify the previous example by adding another word "aver" in our list and then rerun the test. Now, both test cases will fail but they will emit different failure traces.

Without Hamcrest Matchers


java.lang.AssertionError:
at org.junit.Assert.fail(Assert.java:91)
at org.junit.Assert.assertTrue(Assert.java:43)
at org.junit.Assert.assertTrue(Assert.java:54)
at com.shekhar.junit.JUnitMatchersTest.shouldContainAllElementsStartingWithavAndEndingWithe(JUnitMatchersTest.java:43)

Using Hamcrest Matchers


java.lang.AssertionError:
Expected: each (a string starting with "av" and a string ending with "e")
got: <[available, avenge, avenue, average, avert]>

at org.junit.Assert.assertThat(Assert.java:778)
at org.junit.Assert.assertThat(Assert.java:736)
at com.shekhar.junit.JUnitMatchersTest.shouldContainAllElementsStartingWithavAndEndingWithe(JUnitMatchersTest.java:50)

As you can see, the failure trace emitted by Hamcrest is much more descriptive and tells you where the problem is. Any developer can very easily debug the problem using this failure information.

Now I will talk about why you should use matchers in your code. Let’s take a look at matcher support in JUnit.

The Matchers Provided in the JUnit Library

JUnit comes bundled with a subset of Hamcrest matchers so you don't have to add any new dependency in your application. These matchers can be found in the org.hamcrest.core package of the JUnit JAR. Let’s take a look at some of these matchers:

  1. IsEqual: This matcher tests whether a value is equal to another value. This method uses the equals method for testing equality, for example:
    assertThat("shekhar", equalTo("shekhar"));

    It can also be used to match arrays (including multidimensional), for example:


    String[] s1 = {"a", "b"};
    String[] s2 = {"a", "b"};
    assertThat(s2, equalTo(s1));

    It can also be used with multidimensional arrays as follows:


    int[][] i1 = new int[][]{{1, 2}, {3, 4}};
    int[][] i2 = new int[][]{{1, 2}, {3, 4}};
    assertThat(i2, equalTo(i1));
  2. Is: Is is a matcher that decorates another matcher, retaining the behavior but allowing the test to be more expressive. For example:
    assertThat(name, is(equalTo("shekhar")))
  3. IsNot: This matcher can be used when you need to test for a non-equality, for example:
    assertThat("A",not("B"))
    It can be read as A is not equal to B.
  4. IsNull: This matcher can be used when you need to test for nullability. For example:

    assertThat(null,nullValue());
    assertThat("A",notNullValue());
  5. AllOf: This matcher takes multiple matchers and evaluates to true if all of the passed matchers evaluate to true. AllOf uses shortcut evaluation, so subsequent matchers are not called if an earlier matcher evaluate to false. For example:
    assertThat(list, allOf(notNullValue(),hasSize(1)));
  6. IsInstanceOf : This matcher tests whether a value is an instance of class. For example:

    assertThat(1, instanceOf(Number.class));
    assertThat(null, not(instanceOf(String.class)));

JUnit extended these basic matchers and created some custom matchers, which can be found in the org.junit.matchers package. All the matchers are hosted in a JUnitMatchers class. Let’s look at some of these matchers with a unit test.


import static org.junit.matchers.JUnitMatchers.*;

import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.*;

import static org.hamcrest.CoreMatchers.*;
import org.junit.Before;
import org.junit.Test;

public class JUnitMatchersTest {

private List words;

@Before
public void setUp() {
words = Arrays.asList("available", "avenge", "avenue", "average");
}

/**
* This method tests hasItem matcher which takes an element. This matcher
* matches any collection matching an element.
*/
@Test
public void shouldMatchACollectionContainingAElement() {
assertThat(words, hasItem("available"));
}

/**
* This method tests hasItems matcher which takes multiple elements. This
* matcher matches any collection containing every element in the elements.
*/
@Test
public void shouldMatchCollectionContainingMultipleItems() {
assertThat(words, hasItems("available", "avenge", "avenue"));
}

/**
* This method tests hasItem matcher which takes a matcher. This matcher
* matches any collection containing an element matching elementMatcher
*/
@Test
public void shouldMatchCollectionContainingElementMatcher() {
assertThat(words, hasItem(IsEqual.equalTo("available")));
}

/**
* This method tests hasItems matcher which takes multiple matchers.
*/
@SuppressWarnings("unchecked")
@Test
public void shouldMatchCollectionContainingElementMatchers() {
assertThat(
words,
hasItems(containsString("avail"), containsString("ave"),
containsString("average")));
}

/**
* This method tests everyItem matcher. This matcher matches any collection
* in which every element matches elementMatcher.
*/
@Test
public void shouldHaveEveryItemMatchingElementMatcher() {
assertThat(words, everyItem(containsString("av")));
}
}

Getting the Full Power of Hamcrest

So far we have used only a subset of Hamcrest, which comes bundled with JUnit. To get the full power of Hamcrest we have to use the Hamcrest library. You can download the hamcrest-all-1.3.0RC1.jar from code.google.com. This JAR is not present in any Maven repository so you have to manually install it in your Maven repository.


mvn install:install-file -Dfile=hamcrest-all-1.3.0RC1.jar -DgroupId=org.hamcrest -DartifactId=hamcrest-all -Dversion=1.3.0RC1 -Dpackaging=jar

Here are some of the most important matchers that do are not available in the JUnit library.

  • Beans
    • hasProperty -- Tests JavaBeans properties
  • Collections
    • array -- Tests an array's elements against an array of matchers
    • hasEntry, hasKey, hasValue -- Test a map that contains an entry, key or value
    • hasItemInArray -- Test an array that contains an element
  • Number
    • closeTo -- Tests whether floating point values are close to a given value
    • greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo -- Test ordering
  • Text
    • equalToIgnoringCase -- Tests string equality, ignoring case
    • equalToIgnoringWhiteSpace -- Tests string equality, ignoring differences in runs of whitespace
    • containsString, endsWith, startsWith -- Test string matching

Let’s look at some of these matchers more closely:

  1. HasPropertyWithValue : -- This matcher asserts that a JavaBean property on an argument passed to the mock object meets the provided matcher.

    @Test
    public void testHamcrestLibrary() {

    TeamMate actual = new TeamMate("shekhar", "gulati");
    assertThat(actual, hasProperty("firstName",
    IsEqual.equalTo("shekhar")));

    List team = Arrays.asList(new TeamMate("shekhar", "gulati"),new TeamMate("rahul", "sharma"),new TeamMate("sameer", "arora"));
    Matcher withFirstName = hasProperty("firstName", IsEqual.equalTo("shekhar"));
    assertThat(team, hasItem(withFirstName));
    }
  2. IsCollectionWithSize: This matcher checks whether the collection size satisfies a given matcher.
    assertThat(list, hasSize(1));
  3. IsMapContaining: This matcher is for map.

    @Test
    public void testHamcrestMapMethods() {
    Map map = new HashMap();
    map.put("shekhar", "Shekhar Gulati");
    map.put("test", "test123");

    assertThat(map, hasEntry(equalTo("shekhar"), equalTo("Shekhar Gulati")));
    assertThat(map, hasEntry(equalTo("test"), equalTo("test123")));
    assertThat(map, not(hasEntry(equalTo("shekhar"), equalTo("test123"))));
    }
  4. IsIterableContainingInAnyOrder and IsIterableContainingInOrder -- These matchers are used to test whether iterable should be in any order or use natural ordering. You will need to check the use of comparator and compareTo.

    @Test
    public void testIsIterableContainingInAnyOrderAndIsIterableContainingInOrder(){
    List list = Arrays.asList("a","b","c");
    assertThat(list, contains("a","b","c"));
    assertThat(list, contains(equalTo("a"),equalTo("b"),equalTo("c")));
    assertThat(list, containsInAnyOrder("c","a","b"));
    }

    @Test
    public void testHamcrestLibraryIsIterableContainingInOrder() {
    TeamMate shekhar = new TeamMate("shekhar", "gulati");
    TeamMate rahul = new TeamMate("rahul", "sharma");
    TeamMate sameer = new TeamMate("sameer", "arora");
    List team = Arrays.asList(shekhar, rahul, sameer);
    assertThat(team, IsIterableContainingInOrder.contains(new TeamMate(
    "shekhar", "gulati"), new TeamMate("rahul", "sharma"),
    new TeamMate("sameer", "arora")));

    }

While Hamcrest contains lots of built-in matchers, sometimes you will need to write your own matcher to fit your testing needs. (This Hamcrest tutorial explains how to write your own matchers, and you will find a number of Hamcrest extension libraries here.)

Hamcrest vs. FEST-Assert

FEST-Assert is a Java 5 library that provides a fluent interface for writing assertions. Both Hamcrest and FEST-Assert aim to improve the readability and maintainability of tests, but they are implemented in a different way. FEST-Assert uses chaining to build complex assertions whereas Hamcrest uses nesting. Let’s compare a test written using Hamcrest with one written using FEST.

Using Hamcrest


@Test
public void shouldMatchCollectionHasSizeAndContainsMultipleItems() {
List words = Arrays.asList("available", "avenge", "avenue", "average");
assertThat(words, hasItems("available", "avenge", "avenue"));
assertThat(words,hasSize(3));
}

Using FEST-Assert


@Test
public void shouldMatchCollectionHasSizeAndContainsMultipleItems() {
List words = Arrays.asList("available", "avenge", "avenue", "average");
assertThat(words).hasSize(3).contains("available", "avenge", "avenue");
}

With Hamcrest, you cannot nest two matchers (i.e. hasItems and hasSize), whereas with FEST-Assert you can chain the assertions together. Whether you should use Hamcrest or FEST-Assert depends on your style of programming, but I find FEST-Assert easier to. I prefer FEST-Assert because:

  1. Less conceptual weight: When you use Hamcrest, you have to know all the different matchers available in the Hamcrest library. But with FEST-Assert, you have to know just one method (assertThat) and depending upon your parameter the library will give you a set of methods to work on. This reduces the conceptual weight of FEST-Assert.
  2. Chaining is easier to use than nesting: To build complex assertions in Hamcrest, you need to nest one matcher inside another matcher, which is sometimes confusing. Using Hamcrest, it is your responsibility to find which matcher to use for nesting. But with FEST-Assert you just have to type a period (.) and you will get the possible assertion methods for the value passed to assertThat.

FEST-Assert is undoubtedly much easier to use than Hamcrest but it doesn't come bundled with JUnit and you can't write assertions that can test the elements of a collection. For example, you can’t write the test shown below using FEST-Assert.


@Test
public void shouldContainAllElementsStartingWithavAndEndingWithe() {
assertThat(words, everyItem(both(startsWith("av")).and(endsWith("e"))));
}

The FEST-Assert API

The FEST-Assert API, which provides a fluent interface for writing assertions, is very rich and easy to use. It has a similar feel to mocking frameworks such as jMock and EasyMock. To start using FEST, you need to either download it or add its dependency in your Maven POM file.



org.easytesting
fest-assert
1.3

FEST-Assert provides assertions for the following data types:

  • Object
  • String
  • Collection
  • Map
  • Primitives (boolean, int, char, etc.)
  • Arrays of Object
  • Arrays of primitives
  • BufferedImage
  • Throwable
  • File
  • BigDecimal

Let’s rewrite some of the test cases from earlier in the article using matchers with FEST-Assert.


public class FESTAssertTest {

private List words;

@Before
public void setUp() {
words = Arrays.asList("available", "avenge", "avenue", "average");
}

@Test
public void shouldMatchACollectionContainingAElement() {
assertThat(words).isNotEmpty();
assertThat(words).hasSize(4);
assertThat(words).isNotNull();
assertThat(words).contains("available");
assertThat(words).hasSize(4).isNotEmpty();
}

@Test
public void compareTwoArrays(){
String[] s1 = {"a", "b"};
String[] s2 = {"a", "b"};
assertThat(s1).isEqualTo(s2);
}

@Test
public void testAShouldNotBeEqualToB(){
assertThat("A").isNotEqualTo("B");
}

@Test
public void shouldMatchCollectionContainingMultipleItems_FEST() {
assertThat(words).contains("available", "avenge", "avenue");
}

@Test
public void testFestAssertionLibrary(){
assertThat(0).isZero();
TeamMate shekhar = new TeamMate("shekhar", "gulati",26);
TeamMate rahul = new TeamMate("rahul", "sharma",27);
TeamMate sameer = new TeamMate("sameer", "arora",28);
List team = Arrays.asList(shekhar, rahul, sameer);

assertThat(team).contains(new TeamMate("shekhar", "gulati"));
assertThat(team).onProperty("firstName").contains("shekhar");
assertThat(team).isNotEmpty();
assertThat(team).hasSize(3).contains(shekhar,rahul,sameer);
assertThat(team.get(0)).isInstanceOf(TeamMate.class).isEqualTo(shekhar).isNotEqualTo(rahul);

}
}
Quick Tip for Eclipse Users: If you are using Eclipse you can use to shortcut Ctrl+Shift+M (Add Import) to statically import a method. You can also add static members to Java / Editor / Content Assist / Favorites that allow code completion on static methods.

Conclusion

Both Hamcrest and FEST-Assert do a great job of improving Java test readability and maintainability, but each has some limitations. I personally use both of them in my project to get the full power of both. Try them out and write more productive unit tests.

About the Author

Shekhar Gulati is a Java consultant with over 5 years of experience. He is currently working with Xebia India, an Agile software development company. He writes his personal blog at whyjava.wordpress.com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date