04 febrero 2008

JUnit 4.4 y Hamcrest

A partir de la versión 4.4 de JUnit se incorporo al framework un cambio que permite expresar los assertions de una forma más natural. Tomando algunos ejemplos del release notes:

assertThat(x, is(3));
assertThat(x, is(not(4)));
assertThat(responseString, either(containsString("color")).or(containsString("colour")));
assertThat(myList, hasItem("3"));

Este cambio me pareció más que interesante. Primero por que la consecuencia de modelar ciertos conceptos muy abstractos como el chequeo realizado en un assert (llamado Matching en el framework) se obtuvo una interfaz con más "intention revealing" e información. Por ejemplo este tipo de assert puede mostrar mejores mensajes de error:

assertThat(responseString, anyOf(containsString("color"), containsString("colour")));
// ==> failure message:
// java.lang.AssertionError:
// Expected: (a string containing "color" or a string containing "colour")
//      got: "Please choose a font"

La otra consecuencia es que permite expresar más facilmente ciertos asserts que antes eran complicados, por ejemplo para chequear si una colección tiene ciertos elementos puede usarse directamente hasItems donde antes habría que crear una colección a parte y chequear por la inclusión de los elementos (o hacer un assert por elemento).

La buena noticia es que estos asserts especiales pueden utilizarse en versiones más viejas de JUnit ya que fueron "robados" de un proyecto llamado Hamcrest (ver tutorial).

Llevando la misma idea a Smalltalk

Si bien muchas expresiones son más simples en Smalltalk (debido al tipado dinamico y a los keyword messages) algo de este estilo:

assertThat(x, is(not(4)));

No queda de una forma linda:

self assertThat: x matches: (self is: (self not: 4))

Al principio pensé que aprovechando que en Smalltalk se puede "capturar" un DNU, podrían obtenerse expresiones similares. Por ejemplo al capturarse los DNUs para los selectors #assertThat:xxx:yyy:, determinar que Matcher utilizar en base a la porción "xxx:yyy:" del selector, luego el código quedaría asi:

self assertThat: x isNot: 4

Pero como podrán ver en el ejemplo en este caso el isNot no es una composición de Matchers, ouch!. (de todas formas estimo que is(not(4)) es equivalente a not(4)... si no la composición no me cierra, esta forma de hacer el "is not" debe ser solo por motivos de intention revealing, aunque no estoy con un Eclipse para poder comprobarlo).

La primer opción de crear los objetos directamente como en Java da un resultado que no queda tan expresivo, aunque es más simple y más flexible. 

Otra opción muy ala Smalltalk sería agregar metodos a Object para crear los Matchers (en lugar de usar la igualdad común y corriente), por ejemplo:

self assertThat: (x is: 4) negated

El problema de esta opción es que estos métodos para los Matchers tienen sentido solo en el contexto de los tests, por lo tanto agregarlos a Object sería una mala idea.

En fin, este tipo de "DSL Interno" como les llama Fowler (aunque yo prefiero llamarlos "truquitos sintacticos") tiene su particularidad en cada lenguaje. 

En Smalltalk las expresiones utilizando keyword messages y mensajes en cascadas son muy "lindas" para este tipo de cosas, pero en Java también los metodos static en conjunción con el "peligroso" import static, permiten usar Factory Methods para escribir expresiones "lindas".

La cuestión esta en si este tipo de assert agrega valor en Smalltalk. En Java yo creo que agrega mucho valor ya que la sintaxis del lenguaje hace dificil escribir cosas intention revealing y este me parece un lindo "hack" para lograrlas.

Otras caracteristicas de JUnit 4.4

Volviendo a JUnit, otra caracterisca robada de otro framework, es la capacidad de definir ciertos objetos que aplican a ciertos test (es decir definir explicitamente los "fixtures").

Por ejemplo es muy común escribir tests del estilo:

public void testResultWhenUserIsNotLoggedIn() // ....
public void testResultWithManagerUserSession() // ...
public void testResultWithGuestUserSession() // ...

No se si se capta la idea: cada test es para ciertos objetos (en este caso podrían ser sesiones de usuarios en diferentes roles).

Esta nueva caracteristica de JUnit, con el pomposo nombre de "Teorías", permite definir explicitamente los objetos y luego indicar mediante un "assumeThat" para caso aplica el test, seria algo asi:

@DataPoint private static Session nullSession = new NullSession();
public void testResultWhenUserIsNotLoggedIn(Session s) {
  assumeThat(session.notLoggedIn());
  // ....
}

Es decir los assumeThat actuan como predicados que indican que información corresponde al test.

Evidentemente esta caracteristica provee más información estatica sobre el test: un editor puede saber que "datos" se utilizan como entradas de los tests. 

Aunque no veo esta caracteristica con tanto entusiamo, me da la impresión que agrega mucho más complejidad al framework y no es tán facíl darse cuenta para que casos aplica cada test. Además por lo que vi en los ejemplos pareciera que las "Teorias" no están reificadas, eso genera que se usen anotations para cosas que se expresarian mejor de la manera "clasica".

Estos son mis "prejucios" sobre los Theories en JUnit 4.4... sería bueno ver comentarios de como esto funciona en un proyecto con muchos tests, o con "fixtures" complicados (los ejemplos en el sitio usan fixtures muy simples, la mayoría con tipos primitivos).

No hay comentarios.:

Publicar un comentario