Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hamcrest: How to instanceOf and cast for a matcher?

Tags:

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.

like image 282
yankee Avatar asked Jul 20 '17 20:07

yankee


People also ask

Is hamcrest a matcher?

Introduction. Hamcrest is a framework for writing matcher objects allowing 'match' rules to be defined declaratively. There are a number of situations where matchers are invaluable, such as UI validation or data filtering, but it is in the area of writing flexible tests that matchers are most commonly used.

Why are there hamcrest matchers?

Purpose of the Hamcrest matcher framework. Hamcrest is a widely used framework for unit testing in the Java world. Hamcrest target is to make your tests easier to write and read. For this, it provides additional matcher classes which can be used in test for example written with JUnit.

Why assertThat is deprecated?

assertThat method is deprecated. Its sole purpose is to forward the call to the MatcherAssert. assertThat defined in Hamcrest 1.3. Therefore, it is recommended to directly use the equivalent assertion defined in the third party Hamcrest library.

Is assertThat actual is Equalto expected ))) a valid hamcrest assert statement?

assertEquals() is the method of Assert class in JUnit, assertThat() belongs to Matchers class of Hamcrest. Both methods assert the same thing; however, hamcrest matcher is more human-readable. As you see, it is like an English sentence “Assert that actual is equal to the expected value”.


2 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);
like image 50
Magnilex Avatar answered Sep 18 '22 20:09

Magnilex


The problem is that you lose the type information here:

 Object value = 1;

This is an insanely weird line, if you think about it. Here value is the most generic thing possible, nothing can be reasonably told about it, except maybe checking if it's null or checking its string representation if it's not. I'm sort of at loss trying to imagine a legitimate use case for the above line in modern Java.

The obvious fix would be assertThat((Comparable)value, greaterThan(0));

A better fix would be casting to Integer, because you're comparing to an integer constant; strings are also comparable but only between themselves.

If you can't assume that your value is even Comparable, comparing it to anything is pointless. If your test fails on the cast to Comparable, it's a meaningful report that you dynamic casting to Object from something else failed.

like image 38
9000 Avatar answered Sep 17 '22 20:09

9000