A medida que la complejidad de nuestro código aumenta, también tiende a aumentar la complejidad de los tests. Al principio nos va bien con asegurarnos que la salida del método es la esperada. Pero llegado el momento en que necesitamos comprobar qué valores contiene cada campo, o qué información se utiliza para llamar a las otras clases, es hora de empezar a utilizar Matchers.
Los Matchers nos permiten especificar las restricciones o condiciones que deben cumplir los parámetros de entrada. En anteriores posts, cada vez que escribíamos with(any(String.class)), estábamos utilizando un Matcher (uno muy genérico, que permite cualquier cadena como parámetro de entrada).
Imaginemos que queremos asegurarnos que nuestro método getWeatherForecast() invoca al método getWeather() con una instancia de país, cuyo nombre debe ser el mismo que el recibido. Para ello vamos a utilizar el método hasProperty() de la clase org.hamcrest.Matchers:
Ahora tenemos un test que se asegura que nuestro servicio enviará siempre el mismo parámetro y, si en algún momento lo modificamos por error, el test nos avisará.
Como véis, es muy sencillo definir restricciones a los parámetros que utilizamos. Simplemente hay que tener en cuenta algunos detalles, que dependen del Matcher utilizado. Por ejemplo, el método hasProperty() requiere que la clase que estás evaluando (en mi caso, Country) cumpla las convenciones de Sun sobre getters/setters. Si en la clase Country no existieran los métodos getName() y setName(), el test no habría funcionado (y ¡cuidado!, porque este tipo de errores sólo informan que el método no se ha invocado, y no el porqué).
Para completar la explicación del ejemplo, hay que fijarse que el segundo parámetro del método hasProperty() es, a su vez, un Matcher. Allí podríamos definir cualquier restricción que afectara al valor del campo (en este caso forzamos que sea el valor conocido).
Con los Matchers se pueden conseguir unos tests muy completos, y que nos permitan detectar fácilmente si algún cambio en nuestro código rompe con el comportamiento esperado. Os recomiendo mirar todos los métodos disponibles de la clase org.hamcrest.Matchers, entre los que destacan:
Por último, para realizar más de una comprobación por parámetro disponemos del método allOf(). Por ejemplo:
Sin embargo, como la clase Matcher utiliza Generics de forma intensa, os puede dar muchos problemas de compilación, intentando encajar los tipos de datos. Por ejemplo, el siguiente código no compila, ya que se espera un Matcher<Object> en vez de un Matcher<Country> (dichosas reglas de los Generics, que van buscando el "mínimo común múltiplo"):
La única solución que he encontrado es definir cada Matcher por separado, para que los tipos de cada variable se vayan acomodando por pasos, en vez de intentar inferirlos de golpe:
Espero que los Matchers os sirvan de ayuda para definir tests mucho más específicos.
Los Matchers nos permiten especificar las restricciones o condiciones que deben cumplir los parámetros de entrada. En anteriores posts, cada vez que escribíamos with(any(String.class)), estábamos utilizando un Matcher (uno muy genérico, que permite cualquier cadena como parámetro de entrada).
Imaginemos que queremos asegurarnos que nuestro método getWeatherForecast() invoca al método getWeather() con una instancia de país, cuyo nombre debe ser el mismo que el recibido. Para ello vamos a utilizar el método hasProperty() de la clase org.hamcrest.Matchers:
[...] import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import org.hamcrest.Matcher; import org.jmock.*; import org.junit.*; // Omito el resto de imports internos public class BombServiceWithMatchersTest { // Omito la declaración de dependencias/mocks y los métodos setUp() y tearDown() @Test public void testGetWeatherShouldUseTheSameCountry() throws Exception { mockery.checking(new Expectations() { { Matcher<Country> matcher = hasProperty("name", equalTo("Avalon")); exactly(1).of(bombSoapClientMock).getWeather(with(matcher)); will(returnValue(1)); } }); int result = bombService.getWeatherForecast("Avalon"); assertEquals(1, result); } // ... other tests }
Ahora tenemos un test que se asegura que nuestro servicio enviará siempre el mismo parámetro y, si en algún momento lo modificamos por error, el test nos avisará.
Como véis, es muy sencillo definir restricciones a los parámetros que utilizamos. Simplemente hay que tener en cuenta algunos detalles, que dependen del Matcher utilizado. Por ejemplo, el método hasProperty() requiere que la clase que estás evaluando (en mi caso, Country) cumpla las convenciones de Sun sobre getters/setters. Si en la clase Country no existieran los métodos getName() y setName(), el test no habría funcionado (y ¡cuidado!, porque este tipo de errores sólo informan que el método no se ha invocado, y no el porqué).
Para completar la explicación del ejemplo, hay que fijarse que el segundo parámetro del método hasProperty() es, a su vez, un Matcher. Allí podríamos definir cualquier restricción que afectara al valor del campo (en este caso forzamos que sea el valor conocido).
Con los Matchers se pueden conseguir unos tests muy completos, y que nos permitan detectar fácilmente si algún cambio en nuestro código rompe con el comportamiento esperado. Os recomiendo mirar todos los métodos disponibles de la clase org.hamcrest.Matchers, entre los que destacan:
- hasEntry(): muy útil al trabajar con Maps, ya que nos permite definir restricciones sobre claves y valores.
- hasKey(): comprueba si un Map contiene la clave facilitada.
- hasItem(): comprueba si el elemento existe dentro de una List.
- startsWith(): muy interesante al trabajar con Strings.
- ...y muchos más que nos pueden ayudar en cualquier momento...
Por último, para realizar más de una comprobación por parámetro disponemos del método allOf(). Por ejemplo:
allOf(hasKey("key1"), hasKey("key2"));
. Sin embargo, como la clase Matcher utiliza Generics de forma intensa, os puede dar muchos problemas de compilación, intentando encajar los tipos de datos. Por ejemplo, el siguiente código no compila, ya que se espera un Matcher<Object> en vez de un Matcher<Country> (dichosas reglas de los Generics, que van buscando el "mínimo común múltiplo"):
Matcher<Country> matcher = allOf( hasProperty("name", equalTo("Avalon")), hasProperty("id", equalTo("1"))); exactly(1).of(bombSoapClientMock).getWeather(with(matcher));
La única solución que he encontrado es definir cada Matcher por separado, para que los tipos de cada variable se vayan acomodando por pasos, en vez de intentar inferirlos de golpe:
Matcher<Country> m1 = hasProperty("name", equalTo("Avalon")); Matcher<Country> m2 = hasProperty("id", equalTo("1")); Matcher<Country> matcher = allOf(m1, m2); exactly(1).of(bombSoapClientMock).getWeather(with(matcher));
Espero que los Matchers os sirvan de ayuda para definir tests mucho más específicos.
Comentarios
El señor Edrai despues de todo lo que machacaba a los pobres testers de Telefonica I+D (como servidor...) haciendo un blog sobre testing!!!
Que te has pasado al lado oscuro del desarrollo? :p
Tranquilo, que no me he pasado al lado oscuro... en este blog hablaré poquito de Acceptance Testing o parecidos (lo que os machacaba, vaya... jejeje), sino de Unit e Integration Testing.
Ya sabes: development sin tests no es development ;)
Publicar un comentario