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