Хамкрест: Как instanceOf и бросать для матчи?

Вопрос

Предположим, что следующий простой тест:

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

Тест не будет компилироваться, потому что "largeThan" может применяться только к экземплярам типа Comparable. Но я хочу сказать, что value - целое число, большее нуля. Как я могу выразить это с помощью Hamcrest?

То, что я пробовал до сих пор:

Простое решение состоит в том, чтобы просто удалить дженерики, наведя подобный подобный матчи:

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

Возможно, но генерирует предупреждение компилятора и чувствует себя не так.

Достаточная альтернатива:

@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());
            }
        }
    };
}

Чувствует себя "аккуратным" и "правильным", но на самом деле это очень много кода для чего-то, что кажется простым. Я попытался найти что-то вроде встроенного в hamcrest, но я не увенчался успехом, но, может быть, я что-то пропустил?

Фон

В моем фактическом тестовом случае код выглядит следующим образом:

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

В моем упрощенном случае в вопросе я мог бы написать assertThat((Integer)value, greaterThan(0)). В моем фактическом случае я мог написать assertThat((Integer)map.get("the number"), greaterThan(0)));, но это, конечно, ухудшит сообщение об ошибке, если что-то не так.

Ответы

Ответ 1

Проблема заключается в том, что вы теряете информацию о типе здесь:

 Object value = 1;

Это безумно странная линия, если вы думаете об этом. Здесь value является наиболее общей вещью, ничего не может быть разумно сказано об этом, за исключением, может быть, проверки, если он null или проверки его строкового представления, если это не так. Я вроде как потеряюсь, пытаясь представить законный вариант использования для вышеуказанной строки в современной Java.

Очевидным решением будет assertThat((Comparable)value, greaterThan(0));

Лучшее исправление будет отличать от Integer, потому что вы сравниваете целочисленную константу; строки также сопоставимы, но только между собой.

Если вы не можете предположить, что ваш value равно Comparable, сравнение его с чем-либо бессмысленно. Если ваш тест завершился неудачно при трансляции на Comparable, это значимый отчет о том, что динамическое кастинг на Object из чего-то еще не удался.

Ответ 2

Этот ответ будет не показывать, как это сделать, используя Hamcrest, я не знаю, есть ли лучший способ, чем предлагаемый.

Однако, если у вас есть возможность включить еще одну тестовую библиотеку, AssertJ поддерживает именно это:

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));
  }

}

Нет необходимости в кастинге, AssertJ делает это за вас.

Кроме того, он печатает довольно сообщение об ошибке, если утверждение терпит неудачу, при этом value слишком мало:

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

Или, если value не имеет правильного типа:

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

Javadoc для isInstanceOfSatisfying(Class<T> type, Consumer<T> requirements) можно найти здесь, в котором также приведены примеры более сложных утверждений:

// 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);

Ответ 3

Проблема с картой, содержащей значения Object, заключается в том, что вы должны принять для сравнения определенный класс.

Какой недостаток недостатка - это способ превратить сортировщик из заданного типа в другой, например, тот, который находится в этом контексте: 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));
    }
}

С этим вы можете написать свои утверждения таким образом:

@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());
                }
            }))));
}

Но опять же проблема заключается в том, что вам нужно указать данный сравнимый числовой класс (в этом случае целое число).

В случае ошибки утверждения сообщение будет выглядеть следующим образом:

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>]

Ответ 4

Как насчет расширенной версии вашей первоначальной попытки:

@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);
        }
    };
}

Теперь пользовательский макет немного менее подробный, и вы все равно достигаете того, что хотите.

Если значение слишком мало:

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>]

Если значение не соответствует типу:

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>]