Unit Testing with JUnit – Part 3 – Hamcrest Matchers

Unit Testing with JUnit – Part 3 – Hamcrest Matchers

2 Comments

In this series on unit testing with JUnit, we started with JUnit tests both using Maven and IntelliJ in the first part. In the second part, we learned about assertions, JUnit 4 annotations, and test suites. In this post, we will cover assertThat, a more expressive style of assertion that uses Hamcrest matchers.

Assertions with assertThat

The classic JUnit assertions, such as assertEquals, assertTrue, and so on are simple to understand and use. But, by using assertThat with Hamcrest matchers, It’s easy to make dramatic improvements to your tests. Hamcrest is a framework that provides support for Java unit testing. Hamcrest contains self-contained classes, called matchers with static methods designed to be used with JUnit assertThat.

What you can do with the classic assertions, you can also do with assertThat, but more fluently, and make tests more readable. For example, take a look at the following assertions:

. . .
assertFalse(expected.equals(actual));

assertThat(actual, is(not(equalTo(expected))));
. . .

As it is relevant, the second assertion is more readable. If you read out, the second assertion reads more like a sentence – “Assert that actual is not equal to expected”.

Beside test readability, readability of test failures is another highlight of assertThat, as shown in the following figure.

JUnit Assertion Failure Messages for assertFalse and assertThat

As you can see, the second assertion failure message of assertThat is far more explanatory as compared to assertFalse. It was because we used a core Hamcrest matcher.

Core Matchers

When you write an assertThat method, you pass two parameters to it. The first is the actual result, typically the value/object returned by the method under test. The second parameter is a matcher obtained from a call to a matcher method. The matcher is an object that matches the test rule. To see how it works, we will write a class with few methods that we will test.

MatchersDemo.java

package guru.springframework.unittest.matchers;

import java.util.HashSet;
import java.util.Set;

public class MatchersDemo {

    public String toConcatedUpperCase(String str1,String str2){

        return str1.concat(str2).toUpperCase();
    }
    public double floatingPointMultiplication(double num1, double num2) {
        return num1 * num2;
    }

    private Set<String> stringCol = new HashSet<>();
    public boolean addStringToCollection(final String newString)
    {
       return this.stringCol.add(newString);
    }
    public Set<String> getStringCollection()
    {
        return this.stringCol;
    }

}

 

In the class above, we wrote a toConcatedUpperCase() method that concatenates two strings passed as parameters, converts the result to uppercase, and returns it. We then wrote a floatingPointMultiplication() method that returns the product of two double values passed to it. We also wrote the addStringToCollection() and getStringCollection() that adds a string to a Set collection and returns the Set respectively.

We will next write a test class with few test methods.

MatchersDemoTest.java

package guru.springframework.unittest.matchers;

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

public class MatchersDemoTest {
    MatchersDemo matchersDemo;
    @Before
    public void setup(){
        matchersDemo=new MatchersDemo();
    }
   @Test
    public void testToConcatedUpperCase() throws Exception {
        String expected="HELLOWORLD";
        String actual=matchersDemo.toConcatedUpperCase("hello","world");
        assertThat(actual, is(equalTo(expected)));
    }
    @Test
    public void testToConcatedUpperCaseStartWith() throws Exception {
        String expected="HELLO";
        String actual=matchersDemo.toConcatedUpperCase("hello","world");
        assertThat(actual, startsWith(expected));
    }

    @Test
    public void testToConcatedUpperCaseContainsString() throws Exception {
        String expected="WORLD";
        String actual=matchersDemo.toConcatedUpperCase("hello","world");
        assertThat(actual, containsString(expected));
    }
   @Test
    public void testToConcatedUpperCaseForAllMatchers() throws Exception {
        String expected="HELLO";
        String actual=matchersDemo.toConcatedUpperCase("hello","world");
        assertThat(actual, is(allOf(notNullValue(), instanceOf(String.class), startsWith(expected), containsString(expected))));
    }

}

In the test class above, we started with a static import of the Hamcrest core matcher, in Line 6 . We then used the @Before annotation to instantiate the MatchersDemo class. Recall from the previous post that the @Before annotated method will run before each @Test method in the class. Then, we wrote the assertions:

  • Line 18: We wrote an assertThat with the is method containing the equalTo method. We can read it as-“assert that actual (the value that toConcatedUpperCase() method returns) is equal to expected (HELLOWORLD). Although not necessary, many programmers like to use is and other matcher methods together because it makes assertions more readable. This is the very reason for the existence of is and being referred as decorator: to decorate other matchers.
  • Line 24: We used startsWith to assert that the actual string starts with the expected value, HELLO.
  • Line 31: We used containsString to test that the actual string contains the expected value WORLD.
  • Line 37: We wrote an assertThat with allOf, and things get interesting here. allOf takes multiple matcher methods and returns a matcher. This matcher test if the actual result matches all the specified matchers- think about the Java short circuit && operator. So, in a single assertion, we asserted that the actual result is not a null value, is an instance of the String class, and starts with and contains HELLO. The corresponding matcher method, that works like the Java short circuit || operator is anyOf.

When we run the test class above, all the test passes. But, the core matchers we used are only a subset of the wide range of Hamcrest matchers. There are additional matchers for specific test requirements, such as testing collections, numbers, text comparison, and so on. The aditional matchers are not part of JUnit, and to use them, we need to seperately download the Hamcrest matcher library and point the project classpath to it. If you are using Maven, add the following dependency to the pom.xml file.

. . .
<dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-library</artifactId>
        <version>1.3</version>
        <scope>test</scope>
    </dependency>
. . .

Collection Matchers

Collections often have more complex testing needs. For example testing the size of a collection, testing for one or more elements in a collection, their ordering, etc. The Hamcrest collection matchers are designed to support the needs of testing collections in unit tests.
Let’s write a new test class and use the Hamcrest collection matchers.

CollectionMatchersTest

package guru.springframework.unittest.matchers;

import org.junit.Before;
import org.junit.Test;
import java.util.Set;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class CollectionMatchersTest {
    MatchersDemo matchersDemo;
    @Before
    public void setup(){
        matchersDemo=new MatchersDemo();
        matchersDemo.addStringToCollection("First string");
        matchersDemo.addStringToCollection("Second string");
        matchersDemo.addStringToCollection("Third string");
    }

    @Test
    public void testForCollectionSize() throws Exception{
        Set<String> actual=matchersDemo.getStringCollection();
        int expected=3;
        assertThat(actual, hasSize(expected));
    }


    @Test
    public void testForMultipleItemsInCollection()throws Exception{
        Set<String> actual=matchersDemo.getStringCollection();
        String expected1="First string";
        String expected2="Third string";
        assertThat(actual, hasItems(expected1, expected2));
    }

    @Test
    public void testForMultipleItemsWithAnyOrderInCollection() throws Exception{
        Set<String> actual=matchersDemo.getStringCollection();
        String expected1="First string";
        String expected2="Second string";
        String expected3="Third string";
        assertThat(actual, containsInAnyOrder(expected1, expected2, expected3));
    }


}

 

In the test class above, we initialized the Set collection of MatchersDemo with few strings in a @Before method. Then, we wrote the following assertions:

  • Line 23: We wrote a assertThat with hasSize to test the size of the collection.
  • Line 32: We used hasItems to test for multiple items in the collection. To test for a single item, you can use hasItem.
  • Line 41: We used containsInAnyOrder to test that all items in the collection match the expected items, in any order. If you want to test for all items in the same order, use the stricter contains.

Number Matchers

I particularly find the Hamcrest number matchers useful to test floating point calculations that provide accurate approximations, but not exact results. The assertion assertThat(2.32 * 3, equalTo(6.96)); will fail because the actual result is not what we expect (6.96). By, looking at the failure message we will understand the reason.

java.lang.AssertionError: 
Expected: <6.96>
     but: was <6.959999999999999>
. . . .

As you can notice, the actual value is different from what we expected. To test for such floating point calculations, there is a closeTo matcher method that we will cover now.

NumberMatchersTest

package guru.springframework.unittest.matchers;

import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class NumberMatchersTest {
    MatchersDemo matchersDemo;
    double actual;
    @Before
    public void setup(){
        matchersDemo=new MatchersDemo();
        actual= matchersDemo.floatingPointMultiplication(2.32,3);
    }

    @Test
    public void testFloatingPointMultiplication()throws Exception
    {
        assertThat(actual, closeTo(6.96, 0.0005));
    }

    @Test
    public void testFloatingPointMultiplicationGreaterThan()throws Exception
    {
        assertThat(actual, greaterThan(6.0));
    }


}

In Line 20 of the test class above, we used closeTo to test for the result of the floatingPointMultiplication() method under test. The closeTo method matches if an examined double value is equal to the first parameter value, within a range of +/- error specified by the second parameter. We also wrote an assertThat with greaterThan in Line 26 to check if the actual value returned by the method under test is greater than the specified value 6.0.

Some other number matcher methods are greaterThanOrEqualTo, lessThan, and lessThanOrEqualTo. As their names are self-explanatory, I won’t explain them further.

Text Comparison Matchers

We did some text comparisons with the core matchers on the toConcatedUpperCase() method earlier in this post. But, for more flexibility, let’s look at some specific text comparison matchers.

TextComparisionMatchersTest.java

package guru.springframework.unittest.matchers;

import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class TextComparisionMatchersTest {
    MatchersDemo matchersDemo;
    String actual;
    @Before
    public void setup(){
        matchersDemo=new MatchersDemo();
        actual= matchersDemo.toConcatedUpperCase("Hello","World");
    }

    @Test
    public void testIgnoringCase()throws Exception
    {
        assertThat(actual, equalToIgnoringCase("HeLLoWORld"));
    }

    @Test
    public void testIgnoringWhitespace()throws Exception
    {
        assertThat(actual, equalToIgnoringWhiteSpace("  HELLOWORLD "));
    }

    @Test
    public void testContainsInOrder()throws Exception
    {
        assertThat("Actual string under test", stringContainsInOrder(Arrays.asList("Actual", "string", "under","test")));
    }

}

In Line 21 and Line 27 we used equalToIgnoringCase and equalToIgnoringWhiteSpace to test for string equality while ignoring casing and white spaces respectively. In Line 33 we used stringContainsInOrder to test that the actual result contains the specified strings in the same sequence.

The Hamcrest matchers library is big. In this post we looked at few of them. But more importantly, we learned how unit tests are done the real way – Hamcrest matchers.

Summary

As you saw, JUnit’s assertThat combined with Hamcrest matchers has much better functionality. But by saying so, the old assert methods are here to stay. If you are running the old assert methods present in existing test code, you may continue doing so. But, if you plan to write new test code, consider using the Hamcrest matchers. They are more readable – as the JUnit release note says “This syntax allows you to think in terms of subject, verb, object– assert that x is 3”. Also, you will realize the full benefits when your test fails during complex testing. The detailed failure message will point you in the right direction at a very less, or no time at all.

During unit testing enterprise application code using the Spring Framework, you can unleash the potentials of the Hamcrest matchers. Beside the regular assertions, you can use the Hamcrest bean matchers to test properties of Spring beans. You can also use matchers to test whether a view name contains a specific string in Spring MVC, test responses from mock objects. You’ll find the versatility of the Hamcrest matchers very beneficial when writing unit tests in JUnit.

About jt

    You May Also Like

    2 comments on “Unit Testing with JUnit – Part 3 – Hamcrest Matchers

    1. April 25, 2017 at 10:43 am

      Thank you for this great tutorial, I like the way you explain it!!!! Thank you

      Reply
    2. June 28, 2017 at 3:17 pm

      Great tutorial

      Reply

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.