How to use (primitive) autoboxing/widening with Hamcrest?
Asked Answered
E

2

10

I came across https://code.google.com/p/hamcrest/issues/detail?id=130 to add some sugar syntax for Hamcrest matchers. But the idea was rejected by the Hamcrest developers.

Any other smart ideas to make tests better readable by avoiding having to type L behind longs?

@Test
public void test1() {
    int actual = 1;
    assertThat(actual, is(1));
}

@Test
public void test2() {
    long actual = 1L;
    assertThat(actual, is(1)); // fails as expected is <1> but result was <1L> 
    // assertThat(actual, is(1L)); off course works..
}

@Test
public void test3() {
    Long actual = new Long(1);
    assertThat(actual, is(1)); // fails as expected is <1> but result was <1L>
}

UPDATE

See also below the differences when comparing e.g. int and long using default Java laguage (==), standard junit assert (assertTrue) and the hamcrest is() method. It seems odd hamcrest doest not supporting matching/comparing long vs int and the rest is.

@Test
public void test2() {
    long actual = 1L;
    int expected = 1;
    assertTrue(expected == actual); // std java succeeds
    assertEquals(expected, actual); // std junit succeeds
    assertThat(actual, is(expected)); // hamcrest fails: Expected: is <1> but: was <1L>
}
Everglades answered 18/10, 2014 at 8:40 Comment(1)
Simple is() - without explicitly adding L - will not work as explained in exampleEverglades
U
6

This is totally unrelated to the issue you linked which is about addressing faulty static analyzers and was correctly rejected. The issue you're experiencing is common in Java when mixing primitive types.

To avoid typing that L you'd have to provide overloaded versions of all the matchers--not just is. Consider these examples:

assertThat(longValue, greaterThan(1));
assertThat(longList, contains(1, 2, 3));

Update

You can easily add your own overloaded versions to perform the conversion:

public static Matcher<Long> is(Integer value) {
    return org.hamcrest.core.Is.is(value.longValue());
}

Of course, now that you have one to convert from int to long you'll want ones for float and double:

public static Matcher<Long> is(Float value) {
    return org.hamcrest.core.Is.is(value.longValue());
}

public static Matcher<Long> is(Double value) {
    return org.hamcrest.core.Is.is(value.longValue());
}

Since Java won't automatically convert from byte to Integer*, you also needs versions for byte and short. That's ugly enough, but what about casting to the other types, e.g., from int to double?

public static Matcher<Double> is(Integer value) {
    return org.hamcrest.core.Is.is(value.doubleValue());
}

Compile Error: Duplicate method is(Integer)

Uh oh! These won't work because Java doesn't allow you to overload methods based on the return type. You'll have to declare these methods in separate classes which I leave to you.

Given the giant mess this would create, I doubt the Hamcrest authors would be open to such an addition given the low payoff. Honestly, you're much better off being explicit by using 1L and 1.0 as needed.

* Though the compiler will convert from byte to int which could be boxed to Integer.

Unwholesome answered 18/10, 2014 at 18:52 Comment(6)
Maybe I didn't understand the issue I linked then. I see that it proposes to add is(int|long|.. value) like methods.Everglades
Anyway see my update in the question regarding comparing int's vs longs's and the Java/Junit standard behaviour. It's odd Hamcrest thinks differently (/does not support it).Everglades
Java's comparison operators do the conversion as part of the language (type coersion), and JUnit provides a few overloaded forms for convenience. Hamcrest would have to provide a lot more overloaded forms because it has many more matchers.Unwholesome
Yes I understand, but think will improve usability.. especially for reading tests..Everglades
See my update for how you can add your own overloaded methods to handle this. It will require a lot of copy-n-paste work and won't be comprehensive, but it will work.Unwholesome
I agree this will create a giant mess unfortunately. I will keep using the 1L and 1.0 where needed, but some way it feels clumsy.Everglades
W
0

You could write your own Matcher.

public class NumbersMatcher {

public static TypeSafeMatcher<Number> numberEqualsTo(Number actualNumber) {
    return new TypeSafeMatcher<>() {
        @Override
        protected boolean matchesSafely(Number expectedNumber) {
            if(actualNumber instanceof Integer) {
                return Objects.equals(expectedNumber.intValue(), actualNumber);
            } else if(actualNumber instanceof Long) {
                return Objects.equals(expectedNumber.longValue(), actualNumber);
            } else if(actualNumber instanceof Float) {
                return Objects.equals(expectedNumber.floatValue(), actualNumber);
            } else if(actualNumber instanceof Double) {
                return Objects.equals(expectedNumber.doubleValue(), actualNumber);
            }

            return Objects.equals(expectedNumber, actualNumber);
        }

        @Override
        public void describeTo(Description description) {
            description.appendText(actualNumber+"");
        }

        @Override
        protected void describeMismatchSafely(Number number2, Description mismatchDescription) {
            mismatchDescription.appendText(number2+"");
        }

    };
}

}

Wholly answered 7/4, 2022 at 12:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.