Saturday, April 26, 2014

Hamcrest Matching with Lambdas

Using Hamcrest matchers within your JUnit tests is a great way to create understandable, fluent tests.  You can make this test matching special by creating your own matchers specific to the objects you are testing.

In this blog post I will show you how to author custom Hamcrest matchers.  I'll also show how lambdas can make these custom matchers simpler to code and a bit more expressive.

Set-up

I'm using Java 8 in my example.  You will also need to include the appropriate Junit and Hamcrest jars.  The normal Junit jars come with a small portion of the Hamcrest matchers included as a dependency.  I suggest using the Junit jar without Hamcrest and including the full core Hamcrest as a seperate dependency.

My build.gradle file, notice the junit and hamcrest dependencies.

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit-dep:4.+'
    testCompile 'org.hamcrest:hamcrest-core:1.+'
}

In my example we are killing zombies, my Zombie class is here.

Custom Hamcrest Matcher

There are a couple of abstract classes you can use from Hamcrest to build your custom matcher.  I'm using TypeSafeMatcher<T>, the generic is the class for which you are creating the matcher.  This method creates a custom matcher that asserts the type of zombie.

public static Matcher<Zombie> is(Zombie.Type type) {
return new TypeSafeMatcher<Zombie>() {
@Override
protected boolean matchesSafely(Zombie zombie) {
return zombie.getType() == type;
}
@Override
public void describeTo(Description description) {
description.appendText("Zombie should be " + type);
}
@Override
protected void describeMismatchSafely(Zombie zombie, Description description) {
description.appendText("was " + zombie.getType());
}
};
}
view raw Matcher1.java hosted with ❤ by GitHub
Hamcrest ensures your target object is the correct type and is not null.  The remaining methods to create are:
  1. protected boolean matchesSafely(Zombie zombie)
    This is where you do the actual matching to ensure the actual result matches expected.
  2. public void describeTo(Description description)
    These methods are used to build a failure message (see in red below).  This method creates the first line - "Expected: <description>"
  3. protected void describeMismatchSafely(Zombie zombie,    Description description
    This method creates the second line - "but: <description>"
java.lang.AssertionError: 
Expected: Zombie should be ABOMINATION
     but: was WALKER

Test Class using Custom Matcher

package com.tom.zombie;
import org.junit.Test;
import static com.tom.ZombieMatcher.*;
import static com.tom.zombie.Zombie.Type.WALKER;
import static org.junit.Assert.assertThat;
public class ZombieTest {
@Test
public void shouldCreateWalkerZombie() {
Zombie zombie = new Zombie(WALKER);
assertThat(zombie, is(WALKER));
}
}

Custom Hamcrest Matcher using Lambdas

All good stuff and leads to expressive tests.  However, all the boiler-plate code around the custom matcher is tedious.  This is where lambdas can be used to simplify the custom matcher.

First: create a generic matcher that accepts lambdas.

package com.tom;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
public class FuncTypeSafeMatcher<T> extends TypeSafeMatcher<T> {
Predicate<T> matchesSafely;
BiConsumer<T, Description> descibeMismatchSafely;
Consumer<Description> describeTo;
public FuncTypeSafeMatcher(Predicate<T> matchesSafely,
Consumer<Description> describeTo,
BiConsumer<T, Description> descibeMismatchSafely) {
this.matchesSafely = matchesSafely;
this.describeTo = describeTo;
this.descibeMismatchSafely = descibeMismatchSafely;
}
@Override
protected boolean matchesSafely(T item) {
return matchesSafely.test(item);
}
@Override
public void describeTo(Description description) {
describeTo.accept(description);
}
@Override
protected void describeMismatchSafely(T item, Description description) {
descibeMismatchSafely.accept(item, description);
}
}
This class is a template that can be used in multiple custom matchers.  It allows you to use lambda expressions in place of the custom matcher methods.

matchesSafely becomes a Predicate function
describeTo becomes a Consumer function
describeMismatchSafely becomes a BiConsumer (two argument consumer)

Using this class you can instantiate custom matchers with much less code.

Revised Zombie matcher using lambdas and the templates matcher class.

This class implements three custom Zombie matchers in the same space as the original version used to implement one custom matcher.

package com.tom;
import com.tom.zombie.Zombie;
import org.hamcrest.Matcher;
public class ZombieMatcher {
public static Matcher<Zombie> is(Zombie.Type type) {
return new FuncTypeSafeMatcher<Zombie>(z -> z.getType() == type,
(d) -> d.appendText("Zombie should be " + type),
(z, d) -> d.appendText("was " + z.getType()));
}
public static Matcher<Zombie> isKilled() {
return new FuncTypeSafeMatcher<Zombie>(z -> !z.isAlive(),
(d) -> d.appendText("Zombie should be killed"),
(z, d) -> d.appendText("was alive"));
}
public static Matcher<Zombie> isAlive() {
return new FuncTypeSafeMatcher<Zombie>(Zombie::isAlive,
(d) -> d.appendText("Zombie should be alive"),
(z, d) -> d.appendText("was killed"));
}
}

1 comment:

  1. Very cool and just what I was looking for. I'm actually surprised that this isn't built-in to hamcrest. Thanks for sharing

    ReplyDelete