Tuesday, May 29, 2012

JUnit's Built-in Hamcrest Core Matcher Support

In the post Improving On assertEquals with JUnit and Hamcrest, I briefly discussed Hamcrest "core" matchers being "baked in" with modern versions of JUnit. In that post, I focused particularly on use of JUnit's assertThat(T, Matcher) static method coupled with the Hamcrest core is() matcher that is automatically included in later versions of JUnit. In this post, I look at additional Hamcrest "core" matchers that are bundled with recent versions of JUnit.

Two of the advantages of JUnit including Hamcrest "core" matchers out-of-the-box is that there is no need to specifically download Hamcrest and there is no need to include it explicitly on the unit test classpaths. Before looking at more of the handy Hamcrest "core" matchers, it is important to point out here that I am intentionally and repeatedly referring to "core" Hamcrest matchers because recent versions of JUnit only provide "core" (and not all) Hamcrest matchers automatically. Any Hamcrest matchers outside of the core matchers would still need to be downloaded separately and specified explicitly on the unit test classpath. One way to get an idea of what is Hamcrest "core" (and thus what matchers are available by default in recent versions of JUnit) is to look at that package's Javadoc-based API documentation:

From this JUnit-provided documentation for the org.hamcrest.core package, we see that the following matchers (with their descriptions) are available:

ClassJavadoc Class DescriptionCovered Here?
AllOf<T>Calculates the logical conjunction of two matchers.Yes
AnyOf<T>Calculates the logical disjunction of two matchers.Yes
DescribedAs<T>Provides a custom description to another matcher.Yes
Is<T>Decorates another Matcher, retaining the behavior but allowing tests to be slightly more expressive.Again
IsAnything<T>A matcher that always returns true.No
IsEqual<T>Is the value equal to another value, as tested by the Object.equals(java.lang.Object) invokedMethod?Yes
IsInstanceOfTests whether the value is an instance of a class.Yes
IsNot<T>Calculates the logical negation of a matcher.Yes
IsNull<T>Is the value null?Yes
IsSame<T>Is the value the same object as another value?Yes

In my previous post demonstrating the Hamcrest is() matcher used in conjunction with JUnit's assertThat(), I used an IntegerArithmetic implementation as test fodder. I'll use that again here for demonstrating some of the other Hamcrest core matchers. For convenience, that class is reproduced below.

IntegerArithmetic.java
package dustin.examples;

/**
 * Simple class supporting integer arithmetic.
 * 
 * @author Dustin
 */
public class IntegerArithmetic
{
   /**
    * Provide the product of the provided integers.
    * 
    * @param firstInteger First integer to be multiplied.
    * @param secondInteger Second integer to be multiplied.
    * @param integers Integers to be multiplied together for a product.
    * @return Product of the provided integers.
    * @throws ArithmeticException Thrown in my product is too small or too large
    *     to be properly represented by a Java integer.
    */
   public int multiplyIntegers(
      final int firstInteger, final int secondInteger, final int ... integers)
   {
      int returnInt = firstInteger * secondInteger;
      for (final int integer : integers)
      {
         returnInt *= integer;
      }
      return returnInt;
   }
}

In the Improving On assertEquals with JUnit and Hamcrest post, I relied largely on is() to compare expected results to actual results for the integer multiplication being tested. Another option would have been to use the equalTo matcher as shown in the next code listing.

Using Hamcrest equalTo()
   /**
    * Test of multiplyIntegers method, of class IntegerArithmetic, using core
    * Hamcrest matcher equalTo.
    */
   @Test
   public void testWithJUnitHamcrestEqualTo()
   {
      final int[] integers = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
      final int expectedResult = 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 *13 * 14 * 15;
      final int result = this.instance.multiplyIntegers(2, 3, integers);
      assertThat(result, equalTo(expectedResult));
   }

Although not necessary, some developers like to use is and equalTo together because it feels more fluent to them. This is the very reason for is's existence: to make use of other matchers more fluent. I often use is() by itself (implying equalTo()) as discussed in Improving On assertEquals with JUnit and Hamcrest. The next example demonstrates using is() matcher in conjunction with the equalTo matcher.

Using Hamcrest equalTo() with is()
   /**
    * Test of multiplyIntegers method, of class IntegerArithmetic, using core
    * Hamcrest matcher equalTo with "is" Matcher..
    */
   @Test
   public void testWithJUnitHamcrestEqualToAndIsMatchers()
   {
      final int[] integers = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
      final int expectedResult = 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 *13 * 14 * 15;
      final int result = this.instance.multiplyIntegers(2, 3, integers);
      assertThat(result, is(equalTo(expectedResult)));
   }

The equalTo Hamcrest matcher performs a comparison similar to calling Object.equals(Object). Indeed, its comparison functionality relies on use of the underlying object's equals(Object) implementation. This means that the last two examples will pass because the numbers being compared are logically equivalent. When one wants to ensure an even greater identity equality (actually the same objects and not just the same logical content), one can use the Hamcrest sameInstance matcher as shown in the next code listing. The not matcher is also applied because the assertion will be true and the test will pass only with the "not" in place because the expected and actual results happen to NOT be the same instances!

Using Hamcrest sameInstance() with not()
   /**
    * Test of multiplyIntegers method, of class IntegerArithmetic, using core
    * Hamcrest matchers not and sameInstance.
    */
   @Test
   public void testWithJUnitHamcrestNotSameInstance()
   {
      final int[] integers = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
      final int expectedResult = 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 *13 * 14 * 15;
      final int result = this.instance.multiplyIntegers(2, 3, integers);
      assertThat(result, not(sameInstance(expectedResult)));
   }

It is sometimes desirable to control the text that is output from an assertion of a failed unit test. JUnit includes the core Hamcrest matcher asDescribed() to support this. A code example of this is shown in the next listing and the output of that failed test (and corresponding assertion) is shown in the screen snapshot of the NetBeans IDE that follows the code listing.

Using Hamcrest asDescribed() with sameInstance()
   /**
    * Test of multiplyIntegers method, of class IntegerArithmetic, using core
    * Hamcrest matchers sameInstance and asDescribed. This one will assert a
    * failure so that the asDescribed can be demonstrated (don't do this with
    * your unit tests as home)!
    */
   @Test
   public void testWithJUnitHamcrestSameInstanceDescribedAs()
   {
      final int[] integers = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
      final int expectedResult = 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 *13 * 14 * 15;
      final int result = this.instance.multiplyIntegers(2, 3, integers);
      assertThat(result,
                 describedAs(
                    "Not same object (different identity reference)",
                    sameInstance(expectedResult)));
   }

Use of describedAs() allowed the reporting of a more meaningful message when the associated unit test assertion failed.

I am going to now use another contrived class to help illustrate additional core Hamcrest matchers available with recent versions of JUnit. This that "needs testing" is shown next.

SetFactory.java
package dustin.examples;

import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * A Factory that provides an implementation of a Set interface based on
 * supplied SetType that indicates desired type of Set implementation.
 * 
 * @author Dustin
 */
public class SetFactory<T extends Object>
{
   public enum SetType
   {
      ENUM(EnumSet.class),
      HASH(HashSet.class),
      SORTED(SortedSet.class), // SortedSet is an interface, not implementation
      TREE(TreeSet.class),
      RANDOM(Set.class);       // Set is an interface, not a concrete collection

      private Class setTypeImpl = null;

      SetType(final Class newSetType)
      {
         this.setTypeImpl = newSetType;
      }

      public Class getSetImplType()
      {
         return this.setTypeImpl;
      }
   }

   private SetFactory() {}

   public static SetFactory newInstance()
   {
      return new SetFactory();
   }

   /**
    * Creates a Set using implementation corresponding to the provided Set Type
    * that has a generic parameterized type of that specified.
    * 
    * @param setType Type of Set implementation to be used.
    * @param parameterizedType Generic parameterized type for the new set.
    * @return Newly constructed Set of provided implementation type and using
    *    the specified generic parameterized type; null if either of the provided
    *    parameters is null.
    * @throws ClassCastException Thrown if the provided SetType is SetType.ENUM,
    *    but the provided parameterizedType is not an Enum.
    */
   public Set<T> createSet(
      final SetType setType, final Class<T> parameterizedType)
   {
      if (setType == null || parameterizedType == null)
      {
         return null;
      }

      Set<T> newSet = null;
      try
      {
         switch (setType)
         {
            case ENUM:
               if (parameterizedType.isEnum())
               {
                  newSet = EnumSet.noneOf((Class<Enum>)parameterizedType);
               }
               else
               {
                  throw new ClassCastException(
                       "Provided SetType of ENUM being supplied with "
                     + "parameterized type that is not an enum ["
                     + parameterizedType.getName() + "].");
               }
               break;
            case RANDOM:
               newSet = LinkedHashSet.class.newInstance();
               break;
            case SORTED:
               newSet = TreeSet.class.newInstance();
               break;
            default:
               newSet = (Set<T>) setType.getSetImplType().getConstructor().newInstance();
               break;
         }
      }
      catch (  InstantiationException
             | IllegalAccessException
             | IllegalArgumentException
             | InvocationTargetException
             | NoSuchMethodException ex)
      {
         Logger.getLogger(SetFactory.class.getName()).log(Level.SEVERE, null, ex);
      }
      return newSet;
   }
}

The contrived class whose code was just shown provides opportunities to use additional Hamcrest "core" matchers. As described above, it's possible to use all of these matches with the is matcher to improve fluency of the statement. Two useful "core" matchers are nullValue() and notNullValue(), both of which are demonstrated in the next JUnit-based code listing (and is is used in conjunction in one case).

Using Hamcrest nullValue() and notNullValue()
   /**
    * Test of createSet method, of class SetFactory, with null SetType passed.
    */
   @Test
   public void testCreateSetNullSetType()
   {
      final SetFactory factory = SetFactory.newInstance();
      final Set<String> strings = factory.createSet(null, String.class);
      assertThat(strings, nullValue());
   }

   /**
    * Test of createSet method, of class SetFactory, with null parameterized type
    * passed.
    */
   @Test
   public void testCreateSetNullParameterizedType()
   {
      final SetFactory factory = SetFactory.newInstance();
      final Set<String> strings = factory.createSet(SetType.TREE, null);
      assertThat(strings, is(nullValue()));
   }

   @Test
   public void testCreateTreeSetOfStringsNotNullIfValidParams()
   {
      final SetFactory factory = SetFactory.newInstance();
      final Set<String> strings = factory.createSet(SetType.TREE, String.class);
      assertThat(strings, notNullValue());
   }

The Hamcrest matcher instanceOf is also useful and is demonstrated in the next code listing (one example using instanceOf by itself and one example using it in conjunction with is).

Using Hamcrest instanceOf()
   @Test
   public void testCreateTreeSetOfStringsIsTreeSet()
   {
      final SetFactory factory = SetFactory.newInstance();
      final Set<String> strings = factory.createSet(SetType.TREE, String.class);
      assertThat(strings, is(instanceOf(TreeSet.class)));
   }

   @Test
   public void testCreateEnumSet()
   {
      final SetFactory factory = SetFactory.newInstance();
      final Set<RoundingMode> roundingModes = factory.createSet(SetType.ENUM, RoundingMode.class);
      roundingModes.add(RoundingMode.UP);
      assertThat(roundingModes, instanceOf(EnumSet.class));
   }

Many of the Hamcrest core matchers covered so far increase fluency and readability, but I like the next two for even more reasons. The Hamcrest hasItem() matcher checks for the existence of the prescribed item in the collection and the even more useful Hamcrest hasItems() matcher checks for the existence of multiple prescribed items in the collection. It is easier to see this in code and the following code demonstrates these in action.

Using Hamcrest hasItem() and hasItems()
   @Test
   public void testCreateTreeSetOfStringsHasOneOfAddedStrings()
   {
      final SetFactory factory = SetFactory.newInstance();
      final Set<String> strings = factory.createSet(SetType.TREE, String.class);
      strings.add("Tucson");
      strings.add("Arizona");
      assertThat(strings, hasItem("Tucson"));
   }

   @Test
   public void testCreateTreeSetOfStringsHasAllOfAddedStrings()
   {
      final SetFactory factory = SetFactory.newInstance();
      final Set<String> strings = factory.createSet(SetType.TREE, String.class);
      strings.add("Tucson");
      strings.add("Arizona");
      assertThat(strings, hasItems("Tucson", "Arizona"));
   }

It is sometimes desirable to test the result of a certain tested method to ensure that it meets a wide variety of expectations. This is where the Hamcrest allOf matcher comes in handy. This matcher ensures that all conditions (expressed themselves as matchers) are true. This is illustrated in the following code listing, which tests with a single assert that a generated Set is not null, has two specific Strings in it, and is an instance of TreeSet.

Using Hamcrest allOf()
   @Test
   public void testCreateSetAllKindsOfGoodness()
   {
      final SetFactory factory = SetFactory.newInstance();
      final Set<String> strings = factory.createSet(SetType.TREE, String.class);
      strings.add("Tucson");
      strings.add("Arizona");
      assertThat(
         strings,
         allOf(
            notNullValue(), hasItems("Tucson", "Arizona"), instanceOf(TreeSet.class)));
   }

To demonstrate the Hamcrest core "anyOf" matcher provided out-of-the-box with newer versions of JUnit, I am going to use yet another ridiculously contrived Java class that is in need of a unit test.

Today.java
package dustin.examples;

import java.util.Calendar;
import java.util.Locale;

/**
 * Provide what day of the week today is.
 * 
 * @author Dustin
 */
public class Today
{
   /**
    * Provide the day of the week of today's date.
    * 
    * @return Integer representing today's day of the week, corresponding to
    *    static fields defined in Calendar class.
    */
   public int getTodayDayOfWeek()
   {
      return Calendar.getInstance(Locale.US).get(Calendar.DAY_OF_WEEK);
   }
}

Now I need to test that the sole method in the class above returns a valid integer representing a day of the week correctly. I'd like my test(s) to ensure that a valid integer representing a day Sunday through Saturday is returned, but the method being tested is such that it may not be the same day of the week returned on any given test run. The code listing below indicates how this can be tested with the JUnit-included Hamcrest "anyOf" matcher.

Using Hamcrest anyOf()
   /**
    * Test of getTodayDayOfWeek method, of class Today.
    */
   @Test
   public void testGetTodayDayOfWeek()
   {
      final Today instance = new Today();
      final int todayDayOfWeek = instance.getTodayDayOfWeek();
      assertThat(todayDayOfWeek,
                 describedAs(
                    "Day of week not in range.",
                    anyOf(is(Calendar.SUNDAY),
                          is(Calendar.MONDAY),
                          is(Calendar.TUESDAY),
                          is(Calendar.WEDNESDAY),
                          is(Calendar.THURSDAY),
                          is(Calendar.FRIDAY),
                          is(Calendar.SATURDAY))));
   }

While Hamcrest's allOf requires all conditions to match for the assertion to be avoided, the existence of any one condition is sufficient to ensure that anyOf doesn't lead to an assertion of a failure.

My favorite way of determining which core Hamcrest matchers are available with JUnit is to use import completion in my Java IDE. When I statically import the org.hamcrest.CoreMatchers.* package contents, all of the available matchers are displayed. I can look in the IDE to see what the * represents to see what matchers are available to me.

It is nice to have Hamcrest "core" matchers included with JUnit and this post has attempted to demonstrate the majority of these. Hamcrest offers many useful matchers outside of the "core" that are useful as well. More details on these are available in the Hamcrest Tutorial.

2 comments:

Unknown said...

Dustin, I just wanted to let you know that your post showing up in google results led me to describedAs, which gave me the behavior that I was looking for without writing a custom annotator to get a useful message:

private void checkIfFileExists(String filepath, boolean expected) {
assertThat(Files.exists(filepath), describedAs("File %0 exists %1", is(expected), filepath, expected));
}

Thanks for keeping your writing clear and concise, and easily searchable!

@DustinMarx said...

Seth,

I'm glad it was helpful and that you let me know it was helpful.

I hope all is well with you.

Dustin