Thursday, January 17, 2013

Hamcrest Containing Matchers

The Hamcrest 1.3 Javadoc documentation for the Matchers class adds more documentation for several of that class's methods than were available in Hamcrest 1.2. For example, the four overloaded contains methods have more descriptive Javadoc documentation as shown in the two comparison screen snapshots shown next.

Although one can figure out how the "contains" matchers work just by trying them out, the Javadoc in Hamcrest 1.3 makes it easier to read how they work. Most Java developers probably think of behavior like that of String.contains(CharSequence) or Collection.contains(Object) when they think of a contains() method. In other words, most Java developers probably think of "contains" as describing if the String/Collection contains the provided characters/objects among other possible characters/objects. However, for Hamcrest matchers, "contains" has a much more specific meaning. As the Hamcrest 1.3 documentation makes much clearer, the "contains" matchers are much more sensitive to number of items and order of items being passed to these methods.

My examples shown here use JUnit and Hamcrest. It is important to emphasize here that Hamcrest's JAR file must appear on the unit tests' classpath before JUnit's JAR file or else I must use the "special" JUnit JAR file built for use with the standalone Hamcrest JAR. Using either of these approaches avoids the NoSuchMethodError and other errors (suc as org.hamcrest.Matcher.describeMismatch error) resulting from mismatched versions of classes. I have written about this JUnit/Hamcrest nuance in the blog post Moving Beyond Core Hamcrest in JUnit.

The next two screen snapshots indicate the results (as shown in NetBeans 7.3) of the unit test code snippets I show later in the blog to demonstrate Hamcrest containing matchers. The tests are supposed to have some failures (7 tests passing and 4 tests failing) to make it obvious where Hamcrest matchers may not work as one expects without reading the Javadoc. The first image shows only 5 tests passing, 2 tests failing, and 4 tests causing errors. This is because I have JUnit listed before Hamcrest on the NetBeans project's "Test Libraries" classpath. The second image shows the expected results because the Hamcrest JAR occurs before the JUnit JAR in the project's "Test Libaries" classpath.

For purposes of this demonstration, I have a simple contrived class to be tested. The source code for that Main class is shown next.

Main.java
package dustin.examples;

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

/**
 * Main class to be unit tested.
 * 
 * @author Dustin
 */
public class Main
{
   /** Uses Java 7's diamond operator. */
   private Set<String> strings = new HashSet<>();

   public Main() {}

   public boolean addString(final String newString)
   {
      return this.strings.add(newString);
   }

   public Set<String> getStrings()
   {
      return Collections.unmodifiableSet(this.strings);
   }
}

With the class to be tested shown, it is now time to look at building some JUnit-based tests with Hamcrest matchers. Specifically, the tests are to ensure that Strings added via the class's addString(String) method are in its underlying Set and accessible via the getStrings() method. The unit test methods shown next demonstrate how to use Hamcrest matchers appropriately to determine if added Strings are contained in the class's underlying Set

Using Hamcrest contains() Matcher with Single String in Set Works
   /**
    * This test will pass because there is only a single String and so it will
    * contain that single String and order will be correct by implication.
    */
   @Test
   public void testAddStringAndGetStringsWithContainsForSingleStringSoWorks()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, contains("Java"));
   }

The unit test shown above passes because the Set only has one String in it and so the order and number of Strings tested with the contains matcher matches.

Using Hamcrest Contains with Same Number of Elements Works if Order Matches
   /**
    * The "contains" matcher expects exact ordering, which really means it should
    * not be used in conjunction with {@code Set}s. Typically, either this method
    * will work and the method with same name and "2" on end will not work or
    * vice versa.
    */
   @Test
   public void testAddStringAndGetStringsWithContainsForMultipleStringsNotWorks1()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, contains("Java", "Groovy"));
   }

   /**
    * The "contains" matcher expects exact ordering, which really means it should
    * not be used in conjunction with {@code Set}s. Typically, either this method
    * will work and the method with same name and "1" on end will not work or
    * vice versa.
    */
   @Test
   public void testAddStringAndGetStringsWithContainsForMultipleStringsNotWorks2()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, contains("Groovy", "Java"));
   }

The two example unit tests shown above and their resultant output of running those test as shown in the previous screen snapshot show that as long as the number of arguments to the contains() matcher are the same as the number of Strings in the collection being tested, the match may work if the elements tested are in exactly the same order as the elements in the collection. With an unordered Set, this order cannot be relied upon, so contains() is not likely to be a good matcher to use with a unit test on a Set of more than one element.

Using Hamcrest Contains with Different Number of Elements Never Works
   /**
    * Demonstrate that contains will NOT pass when there is a different number
    * of elements asked about contains than in the collection.
    */
   @Test
   public void testAddStringAndGetStringsWithContainsNotWorksDifferentNumberElements1()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, contains("Java"));
   }

   /**
    * Demonstrate that contains will NOT pass when there is a different number
    * of elements asked about contains than in the collection even when in
    * different order.
    */
   @Test
   public void testAddStringAndGetStringsWithContainsNotWorksDifferentNumberElements2()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, contains("Groovy"));
   }

As the JUnit test results indicate, these two unit tests never pass because the number of elements being tested for in the Set is fewer than the number of elements in the Set. In other words, this proves that the contains() matcher does not test simply for a given element being in a collection: it tests for all specified elements being present and in the specified order. This might be too limiting in some cases, so now I'll move onto some other matches Hamcrest provides for determining if an element is contained in a particular collection.

Using Hamcrest's containsInAnyOrder() Matcher

The containsInAnyOrder matcher is not as strict as the contains() matcher: it allows tested elements to be in any order within the containing collection to pass.

   /**
    * Test of addString and getStrings methods of class Main using Hamcrest
    * matcher containsInAnyOrder.
    */
   @Test
   public void testAddStringAndGetStringsWithContainsInAnyOrder()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultCSharp = subject.addString("C#");
      final boolean resultGroovy = subject.addString("Groovy");
      final boolean resultScala = subject.addString("Scala");
      final boolean resultClojure = subject.addString("Clojure");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, containsInAnyOrder("Java", "C#", "Groovy", "Scala", "Clojure"));
   }

   /**
    * Use containsInAnyOrder and show that order does not matter as long as
    * all entries provided are in the collection in some order.
    */
   @Test
   public void testAddStringAndGetStringsWithContainsInAnyOrderAgain()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, containsInAnyOrder("Java", "Groovy"));
      assertThat(strings, containsInAnyOrder("Groovy", "Java"));
   }

The two unit tests shown immediately above both pass despite the Strings being tested being provided to the containsInAnyOrder() matcher in a different order than what they could exist in for both collections. However, the less strict containsInAnyOrder() matcher still requires all elements of the containing collection to be specified to pass. The following unit test does not pass because this condition is not met.

   /**
    * This will fail because containsInAnyOrder requires all items to be matched
    * even if in different order. With only one element being tried and two
    * elements in the collection, it will still fail. In other words, order
    * does not matter with containsInAnyOrder, but all elements in the collection
    * still need to be passed to the containsInAnyOrder matcher, just not in the
    * exact same order.
    */
   @Test
   public void testAddStringAndGetStringsWithContainsInAnyOrderDiffNumberElements()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, containsInAnyOrder("Java"));
   }
Hamcrest hasItem() and hasItems() Matchers Work As Sounds

As shown in the next two unit test methods (both of which pass), the Hamcrest hasItem() (for single item) and hasItems (for multiple items) successfully tests whether a collection has the one or more than one specified items respectively without regard for order or number of specified items. This really works more like most Java developers are used to "contains" working when working with Strings and collections.

   /**
    * Demonstrate hasItem() will also work for determining a collection contains
    * a particular item.
    */
   @Test
   public void testAddStringAndGetStringsWithHasItem()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, hasItem("Groovy"));
      assertThat(strings, hasItem("Java"));
   }

   /**
    * Demonstrate that hasItems works for determining that a collection has one
    * or more items and that the number of items and the order of the items
    * is not significant in determining pass/failure.
    */
   @Test
   public void testAddStringAndGetStringsWithHasItems()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat(strings, hasItems("Groovy", "Java"));
      assertThat(strings, hasItems("Java", "Groovy"));
      assertThat(strings, hasItems("Groovy"));
      assertThat(strings, hasItems("Java"));
   }
Hamcrest isIn() Matcher Tests Containment from Other Direction

The just-discussed hasItem() and hasItems() matchers are less strict than contains() and even less strict than containsInAnyOrder() and are often what one wants when one wants to simply ensure that one or multiple items are somewhere in a collection without concern about the item's order in that collection or that other possible items are in that collection. One other way to use Hamcrest to determine the same relationship, but from the opposite perspective, is to use isIn matcher. The isIn matcher determines if an item is somewhere with the collection provided to the matcher without regard for that item's order in the collection or whether or not there are other items in that containing collection.

   /**
    * Use isIn matcher to test individual element is in provided collection.
    */
   @Test
   public void testAddStringAndGetStringsWithIsIn()
   {
      final Main subject = new Main();
      final boolean resultJava = subject.addString("Java");
      final boolean resultGroovy = subject.addString("Groovy");
      final Set<String> strings = subject.getStrings();
      assertThat("Groovy", isIn(strings));
      assertThat("Java", isIn(strings));
   }
Conclusion

Hamcrest provides a rich set of matchers that can be used to determine if specified elements reside within a specified collection. Here are important points to keep in mind when deciding to apply these and determining which to use:

  • Ensure that the Hamcrest JAR is on the test classpath before the JUnit JAR.
  • Use contains when you want to ensure that the collection contains all specified items and no other items and you want the collection to contain the items in the specified order.
    • Generally avoid using contains() matcher with Sets because they are unordered by nature.
  • Use containsInAnyOrder matcher when you still want to strictly test for presence of exactly same items in collection as specified in test, but don't care about the order (applicable for Sets).
  • Use hasItem() and hasItems() matchers to ask a collection if it contains, possibly among other unlisted items and in no particular order, the specified item or items.
  • Use isIn() matcher to ask if a particular item is in the specified collection with no regard for whether other items are in that collection or what order that item is in within the containing collection.

1 comment:

kospiotr said...

Thank you for sharing. Very good explanation :)