tutorial org matchers equalto java hamcrest

java - org - Hamcrest: ¿Cómo instanciar y lanzar para un matcher?



hamcrest tutorial (4)

¿Qué tal una versión ligeramente modificada de su intento original:

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

Ahora el emparejador personalizado es un poco menos detallado y aún así logras lo que quieres.

Si el valor es demasiado pequeño:

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

Si el valor es incorrecto escribe:

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

Pregunta

Supongamos la siguiente prueba simple:

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

La prueba no se compilará, porque "greaterThan" solo se puede aplicar a las instancias del tipo Comparable . Pero quiero afirmar que el value es un entero que es mayor que cero. ¿Cómo puedo expresar eso usando Hamcrest?

Lo que intenté hasta ahora:

La solución simple sería simplemente eliminar los genéricos lanzando el emparejador así:

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

Posible, pero genera una advertencia del compilador y se siente mal.

Una alternativa larga es:

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

Se siente "ordenado" y "correcto", pero en realidad es mucho código para algo que parece simple. Intenté encontrar algo así incorporado en Hamcrest, pero no tuve éxito, pero ¿tal vez me perdí algo?

Fondo

En mi caso de prueba real el código es así:

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

En mi caso simplificado en la pregunta, también podría escribir assertThat((Integer)value, greaterThan(0)) . En mi caso real, podría escribir assertThat((Integer)map.get("the number"), greaterThan(0))); , pero eso, por supuesto, empeoraría el mensaje de error si algo está mal.


El problema con el mapa que contiene valores de objetos es que debe asumir la clase específica para comparar.

Lo que le falta a Hamcrest es una forma de transformar un emparejador de un tipo dado a otro, como el que se encuentra en esta lista: 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)); } }

Con eso, podrías escribir tus afirmaciones de esta manera:

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

Pero el problema, nuevamente, es que necesita especificar una clase numérica Comparable dada (Integer en este caso).

En caso de error de aserción el mensaje sería:

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


El problema es que pierdes la información de tipo aquí:

Object value = 1;

Esta es una línea increíblemente rara, si lo piensas. Aquí el value es lo más genérico posible, nada se puede decir razonablemente al respecto, excepto quizás verificar si es null o verificar su representación de cadena si no lo es. Estoy un poco perdido tratando de imaginar un caso de uso legítimo para la línea anterior en Java moderno.

La solución obvia sería assertThat((Comparable)value, greaterThan(0));

Una mejor solución sería convertir a Integer , porque se está comparando con una constante entera; Las cuerdas también son comparables pero solo entre ellas.

Si no puedes asumir que tu value es incluso Comparable , compararlo con algo no tiene sentido. Si su prueba falla en la conversión a Comparable , es un informe significativo que su conversión dinámica a Object de otra cosa falló.


Esta respuesta no mostrará cómo hacer esto usando Hamcrest, no sé si hay una manera mejor que la propuesta.

Sin embargo, si tiene la posibilidad de incluir otra biblioteca de prueba, AssertJ admite exactamente esto:

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 hace falta ningún casting, AssertJ hace esto por ti.

Además, imprime un mensaje de error bonito si falla la aserción, con un value demasiado pequeño:

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

O si el value no es del tipo correcto:

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

here se puede encontrar el Javadoc para isInstanceOfSatisfying(Class<T> type, Consumer<T> requirements) , que también contiene algunos ejemplos de aseveraciones ligeramente más complicadas:

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