Hiển thị các bài đăng có nhãn JUnit. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn JUnit. Hiển thị tất cả bài đăng

Thứ Năm, 17 tháng 1, 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.

Thứ Hai, 18 tháng 6, 2012

Moving Beyond Core Hamcrest in JUnit

In the post Improving On assertEquals with JUnit and Hamcrest I introduced use of Hamcrest with JUnit. I then looked at JUnit's Built-in Hamcrest Core Matcher Support. In this post, I look at how to apply Hamcrest's non-core matchers with JUnit. These non-core matchers are NOT included with JUnit by default, but are available by including a Hamcrest JAR in the classpath.

Although JUnit's inclusion of Hamcrest core matchers makes them easier to use if one only wants to use the core matchers, this inclusion can make use of the non-core matchers more difficult and is a well-known issue.

Because the non-core Hamcrest matchers are not included with JUnit, the Hamcrest JAR needs to be downloaded. For my examples in this post, I am using hamcrest-all-1.2.jar.

The next screen snapshot indicates the problems with combining the hamcrest-all JAR with the normal JUnit library (JUnit 4.10 as provided by NetBeans 7.2 beta in my example). As the screen snapshot indicates, when the junit-4.10.jar is included in the NetBeans libraries BEFORE the hamcrest-all-1.2.jar, the previously working code (from my previous post) breaks. Both NetBeans and the command-line compiler show this breakage in this screen snapshot.

Switching the order of the test libraries so that the Hamcrest library is listed first and the JUnit JAR listed after it, makes the compiler break on the test code go away. This is shown in the next screen snapshot.

Although switching the order of the dependent libraries so that the Hamcrest JAR is included before the JUnit JAR does prevent the build problem, this is not typically a satisfactory approach. This approach is too fragile for long-term maintainability. Fortunately, there is a better approach that JUnit directly supports to deal with this issue.

A special Hamcrest-less JUnit JAR can be downloaded. The next screen snapshot shows the one I use in this example: junit-dep-4.10.jar. The -dep in the JAR name is the clue that it's Hamcrest-free. The notation next to the JAR on the download page (screen snapshot shown next) points this out as well ("Jar without hamcrest").

With the Hamcrest-free "dep" version of the JUnit JAR, I can include it in the test libraries at any point I like with relation to the Hamcrest JAR and will still be able to build the test code. This is a much more favorable approach than relying on a specific order of test libraries. The next image shows the screen snapshot of NetBeans and the command-line build being successful even with the JUnit JAR listed first.

With the appropriate libraries in use (JUnit-dep JAR and the Hamcrest "all" JAR), all of Hamcrest's matchers can be used with JUnit-based tests. Hamcrest provides numerous matchers beyond the core matches that are now bundled with JUnit. One way to get an idea of the additional matchers available is to look at the classes in the Hamcrest JAR. The following is output from running a jar tvf command against the Hamcrest JAR and removing many of the entries to leave some of the most interesting ones. The "core" matchers tend to be based on the classes in the "core" package and the non-core matchers tend to be based on the classes in all the other packages without "core" in their name.


4029 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/AllOf.java
3592 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/AnyOf.java
1774 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/CombinableMatcher.java
1754 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/DescribedAs.java
1104 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/Every.java
2088 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/Is.java
1094 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/IsAnything.java
2538 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/IsCollectionContaining.java
1862 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/IsEqual.java
2882 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/IsInstanceOf.java
1175 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/IsNot.java
1230 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/IsNull.java
960 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/IsSame.java
675 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/StringContains.java
667 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/StringEndsWith.java
678 Thu May 21 23:21:20 MDT 2009 org/hamcrest/core/StringStartsWith.java

2557 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsArray.java
1805 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsArrayContaining.java
1883 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsArrayContainingInAnyOrder.java
1765 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsArrayContainingInOrder.java
1388 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsArrayWithSize.java
1296 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsCollectionWithSize.java
812 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsEmptyCollection.java
866 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsEmptyIterable.java
1086 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsIn.java
3426 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsIterableContainingInAnyOrder.java
3479 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsIterableContainingInOrder.java
993 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsIterableWithSize.java
1899 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsMapContaining.java
1493 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsMapContainingKey.java
1421 Thu May 21 23:21:20 MDT 2009 org/hamcrest/collection/IsMapContainingValue.java

1380 Thu May 21 23:21:20 MDT 2009 org/hamcrest/number/IsCloseTo.java
2878 Thu May 21 23:21:20 MDT 2009 org/hamcrest/number/OrderingComparison.java

1082 Thu May 21 23:21:20 MDT 2009 org/hamcrest/object/HasToString.java
918 Thu May 21 23:21:20 MDT 2009 org/hamcrest/object/IsCompatibleType.java
2080 Thu May 21 23:21:20 MDT 2009 org/hamcrest/object/IsEventFrom.java

1164 Thu May 21 23:21:20 MDT 2009 org/hamcrest/text/IsEmptyString.java
1389 Thu May 21 23:21:20 MDT 2009 org/hamcrest/text/IsEqualIgnoringCase.java
2058 Thu May 21 23:21:20 MDT 2009 org/hamcrest/text/IsEqualIgnoringWhiteSpace.java
1300 Thu May 21 23:21:20 MDT 2009 org/hamcrest/text/StringContainsInOrder.java

4296 Thu May 21 23:21:20 MDT 2009 org/hamcrest/xml/HasXPath.java

JUnit's providing of a JAR without Hamcrest automatically built in (the "dep" JAR) allows developers to more carefully building up their classpaths if Hamcrest matchers above and beyond the "core" matchers are desired for use with JUnit.

Thứ Ba, 29 tháng 5, 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.

Thứ Hai, 23 tháng 4, 2012

Improving On assertEquals with JUnit and Hamcrest

In my blog post Are Static Imports Becoming Increasingly Accepted in Java?, I discussed the increasing use of static imports in Java to make code more fluent in certain contexts. Unit testing in Java has been particularly affected by the static import and in this blog post I provide one quick example of using static imports to make more fluent unit tests that use JUnit and Hamcrest.

The next code listing is a simple IntegerArithmetic class that has one method that needs to be unit tested.

IntegerArithmetic.java

package dustin.examples;

/**
* Simple class supporting integer arithmetic.
*
* @author Dustin
*/
public class IntegerArithmetic
{
/**
* Provide the product of the provided integers.
*
* @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 ... integers)
{
int returnInt = 1;
for (final int integer : integers)
{
returnInt *= integer;
}
return returnInt;
}
}

A common approach for testing one aspect of the above method is shown next.


/**
* Test of multipleIntegers method, of class IntegerArithmetic, using standard
* JUnit assertEquals.
*/
@Test
public void testMultipleIntegersWithDefaultJUnitAssertEquals()
{
final int[] integers = {2, 3, 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(integers);
assertEquals(expectedResult, result);
}

In the fairly typical unit test example shown above, JUnit's assertEquals is called in a fluent fashion because of the static import of org.junit.Assert.* (not shown). However, recent versions of JUnit (JUnit 4.4+) have begun including Hamcrest core matchers and this allows for an even more fluent test as depicted in the next code snippet.


/**
* Test of multipleIntegers method, of class IntegerArithmetic, using core
* Hamcrest matchers included with JUnit 4.x.
*/
@Test
public void testMultipleIntegersWithJUnitHamcrestIs()
{
final int[] integers = {2, 3, 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(integers);
assertThat(result, is(expectedResult));
}

In this example, JUnit's assertThat (also available as part of the static import of org.junit.Assert.* since JUnit 4.4) is used in conjunction with the included Hamcrest core matcher is(). It's certainly a matter of taste, but I prefer this second approach as more readable to me. Asserting that something (the result) is something else (the expected) seems more readable and more fluent than the older approach. It can sometimes be tricky to remember whether to list the expected or actual result first when using assertEquals and the combination of assertThat and is() makes for a little less work when I write and read tests. Even a little less work, especially when multiplied by numerous tests, is welcome.