Hamcrest: How to instanceOf and cast for a matcher?

Viewed 5609

Question

Assume the following simple test:

@Test
public void test() throws Exception {
    Object value = 1;
    assertThat(value, greaterThan(0));
}

The test won't compile, because "greaterThan" can only be applied to instances of type Comparable. But I want to assert that value is an Integer which is greater than zero. How can I express that using Hamcrest?

What I tried so far:

The simple solution would be to simply remove the generics by casting the matcher like that:

assertThat(value, (Matcher)greaterThan(0));

Possible, but generates a compiler warning and feels wrong.

A lengthy alternative is:

@Test
public void testName() throws Exception {
    Object value = 1;

    assertThat(value, instanceOfAnd(Integer.class, greaterThan(0)));
}

private static<T> Matcher<Object> instanceOfAnd(final Class<T> clazz, final Matcher<? extends T> submatcher) {
    return new BaseMatcher<Object>() {
        @Override
        public boolean matches(final Object item) {
            return clazz.isInstance(item) && submatcher.matches(clazz.cast(item));
        }

        @Override
        public void describeTo(final Description description) {
            description
                .appendText("is instanceof ")
                .appendValue(clazz)
                .appendText(" and ")
                .appendDescriptionOf(submatcher);
        }

        @Override
        public void describeMismatch(final Object item, final Description description) {
            if (clazz.isInstance(item)) {
                submatcher.describeMismatch(item, description);
            } else {
                description
                    .appendText("instanceof ")
                    .appendValue(item == null ? null : item.getClass());
            }
        }
    };
}

Feels "tidy" and "correct", but it is really a lot of code for something that seems simple. I attempted to find something like that built-in in hamcrest, but I was not successful, but maybe I missed something?

Background

In my actual test case the code is like this:

Map<String, Object> map = executeMethodUnderTest();
assertThat(map, hasEntry(equalTo("the number"), greaterThan(0)));

In my simplified case in the question I could also write assertThat((Integer)value, greaterThan(0)). In my actual case I could write assertThat((Integer)map.get("the number"), greaterThan(0)));, but that would of course worsen the error message if something is wrong.

4 Answers

This answer will not show how to do this using Hamcrest, I do not know if there is a better way than the proposed.

However, if you have the possibility to include another test library, AssertJ supports exactly this:

import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class TestClass {

  @Test
  public void test() throws Exception {
    Object value = 1;
    assertThat(value).isInstanceOfSatisfying(Integer.class, integer -> assertThat(integer).isGreaterThan(0));
  }

}

No need for any casting, AssertJ does this for you.

Also, it prints a pretty error message if the assertion fails, with value being too small:

java.lang.AssertionError:
Expecting:
 <0>
to be greater than:
 <0> 

Or if value is not of the correct type:

java.lang.AssertionError: 
Expecting:
 <"not an integer">
to be an instance of:
 <java.lang.Integer>
but was instance of:
 <java.lang.String>

The Javadoc for isInstanceOfSatisfying(Class<T> type, Consumer<T> requirements) can be found here, which also contains some examples of sligthly more complicated assertions:

// second constructor parameter is the light saber color
Object yoda = new Jedi("Yoda", "Green");
Object luke = new Jedi("Luke Skywalker", "Green");

Consumer<Jedi> jediRequirements = jedi -> {
  assertThat(jedi.getLightSaberColor()).isEqualTo("Green");
  assertThat(jedi.getName()).doesNotContain("Dark");
};

// assertions succeed:
assertThat(yoda).isInstanceOfSatisfying(Jedi.class, jediRequirements);
assertThat(luke).isInstanceOfSatisfying(Jedi.class, jediRequirements);

// assertions fail:
Jedi vader = new Jedi("Vader", "Red");
assertThat(vader).isInstanceOfSatisfying(Jedi.class, jediRequirements);
// not a Jedi !
assertThat("foo").isInstanceOfSatisfying(Jedi.class, jediRequirements);

How about a slighly modified version of your original attempt:

@Test
public void testName() throws Exception {
    Map<String, Object> map = executeMethodUnderTest();

    assertThat(map, hasEntry(equalTo("the number"),
            allOf(instanceOf(Integer.class), integerValue(greaterThan(0)))));
}

private static<T> Matcher<Object> integerValue(final Matcher<T> subMatcher) {
    return new BaseMatcher<Object>() {
        @Override
        public boolean matches(Object item) {
            return subMatcher.matches(Integer.class.cast(item));
        }

        @Override
        public void describeTo(Description description) {
            description.appendDescriptionOf(subMatcher);
        }

        @Override
        public void describeMismatch(Object item, Description description) {
            subMatcher.describeMismatch(item, description);
        }
    };
}

Now the custom matcher is a little less verbose and you still achieve what you want.

If the value is too small:

java.lang.AssertionError: 
Expected: map containing ["the number"->(an instance of java.lang.Integer and a value greater than <0>)]
     but: map was [<the number=0>]

If the value is wrong type:

java.lang.AssertionError: 
Expected: map containing ["the number"->(an instance of java.lang.Integer and a value greater than <0>)]
     but: map was [<the number=something>]

The problem with map containing Object values is that you have to assume the specific class to compare.

What hamcrest is lacking is a way to transform a matcher from a given type to another, such as the one in this gist: https://gist.github.com/dmcg/8ddf275688fd450e977e

public class TransformingMatcher<U, T> extends TypeSafeMatcher<U> {
    private final Matcher<T> base;
    private final Function<? super U, ? extends T> function;

    public TransformingMatcher(Matcher<T> base, Function<? super U, ? extends T> function) {
        this.base = base;
        this.function = function;
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("transformed version of ");
        base.describeTo(description);
    }

    @Override
    protected boolean matchesSafely(U item) {
        return base.matches(function.apply(item));
    }
}

With that, you could write your asserts this way:

@Test
public void testSomething() {
    Map<String, Object> map = new HashMap<>();
    map.put("greater", 5);

    assertThat(map, hasEntry(equalTo("greater"), allOf(instanceOf(Number.class),
            new TransformingMatcher<>(greaterThan((Comparable)0), new Function<Object, Comparable>(){
                @Override
                public Comparable apply(Object input) {
                    return Integer.valueOf(input.toString());
                }
            }))));
}

But the problem, again, is that you need to specify a given Comparable numeric class (Integer in this case).

In case of assertion error the message would be:

java.lang.AssertionError
Expected: map containing ["string"->(an instance of java.lang.Number and transformed version of a value greater than <0>)]
     but: map was [<string=NaN>]
Related