furniture
Inflatable Water Slide

Unit Tests (II): clases sin interfaz

October 17th, 2009
Sigamos con los tests unitarios: ¿qué ocurre si alguna de mis dependencias no tiene interfaz? O simplemente, ¿qué ocurre si no me gusta añadir una interfaz a cada una de mis clases?

Aunque recomiendo encarecidamente el uso de interfaces (ayudan a definir y delimitar el alcance de una clase), no es del todo extraño que algunas de nuestras dependencias no las tengan. Como ejemplo, veamos una que seguro que a muchos nos ha tocado implementar: un cliente para leer los mensajes de Twitter. Si decidimos usar el proyecto Twitter4J, nos encontramos con que su cliente twitter4j.Twitter no tiene interfaz.

Si intentáramos hacer un test usando la guía del post anterior:
    [...]
private TwitterService twitterService = null;

private Mockery mockery = new Mockery();
private Twitter twitterMock = null;

@Before
public void setUp() {
twitterService = new TwitterService();

twitterMock = mockery.mock(Twitter.class);
twitterService.setTwitter(twitterMock);
}
[...]

nos lanzaría la siguiente excepción:

java.lang.IllegalArgumentException: twitter4j.Twitter is not an interface
at java.lang.reflect.Proxy.getProxyClass(Proxy.java:362)
at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:581)
at org.jmock.lib.JavaReflectionImposteriser.imposterise(JavaReflectionImposteriser.java:31)
at org.jmock.Mockery.mock(Mockery.java:139)
at org.jmock.Mockery.mock(Mockery.java:120)
[...]

Por suerte, JMock ha pensado en nosotros (JMock Cookbook, capítulo 18) y nos permite definir Expectations sobre una clase o clase abstracta, dejando a un lado la API estándar de Reflection de Java y modificando/reescribiendo el bytecode en tiempo de ejecución. Entonces, ¿qué hay que hacer para conseguir hacer mock de una clase sin interfaz?

En primer lugar, hay que añadir las siguientes bibliotecas a nuestro classpath:
  • jmock-legacy (la misma versión que JMock. En este caso la 2.5.1 aunque funciona con toda la rama 2.x).
  • cglib-nodep (probado con la versión 2.2, funciona con todas las 2.x).
  • objenesis (probado con la versión 1.2, funciona con todas las 1.x).

Y en segundo lugar, modificar la creación del objeto Mockery del siguiente modo:

[...]
import org.jmock.Mockery;
import org.jmock.Expectations;
import org.jmock.lib.legacy.ClassImposteriser;
[...]

public class TwitterServiceTest {

private TwitterService twitterService = null;

private Mockery mockery = new Mockery() {
{
setImposteriser(ClassImposteriser.INSTANCE);
}
};

private Twitter twitterMock = null;

@Before
public void setUp() {
twitterService = new TwitterService();

twitterMock = mockery.mock(Twitter.class);
twitterService.setTwitter(twitterMock);
}
[...]

Como podéis ver, sólo cambia el modo de instanciar el objeto Mockery. Para inicializar los mock-objects de nuestras dependencias, se sigue utilizando el mismo método: context.mock(twitter4j.Twitter.class);. Del mismo modo la definición de Expectations de cada test sigue funcionando igual (recibir un mensaje, una excepción, enviar una actualización, ...):

    @Test
public void testGetMessages() throws Exception {
mockery.checking(new Expectations() {
{
List<Object> statuses = new ArrayList<Object>();
for (int i = 0; i < 10; i++) {
statuses.add(new Object());
}
exactly(1).of(twitterMock).getUserTimeline();
will(returnValue(statuses));
}
});

List<Status> result = twitterService.getMessages();
assertNotNull(result);
assertEquals(10, result.size());
}
Justamente esta clase Twitter forma parte de las dependencias difíciles de integrar en nuestros tests. Parece que intenten ir en contra de cada uno de los consejos para conseguir código testable (http://misko.hevery.com/code-reviewers-guide/). Por ejemplo, el hecho de añadir lógica y excepciones en sus constructores nos hace muy complicado poder crear un objeto twitter4j.Status, ya que necesita una cadena JSON bastante complicada...De todos modos, como estamos probando una clase nuestra que tiene la dependencia con el cliente Twitter, nos interesa ver cómo se comporta ante cada una de las situaciones esperadas (excepciones de conexión, de autorización, resultados nulos, etc). Dependiendo de lo queramos implementar, sólo un pequeño número de tests necesitarán probar respuestas concretas (p.ej., qué ocurre cuando se recibe una fecha incorrecta o un campo vacío).Si queremos probar casos concretos de respuesta, siempre se puede crear una cadena JSON completa, de ejemplo, y utilizarla para crear las distintas instancias de twitter4j.Status que necesitemos dentro de las Expectations. Con estos resultados "simulados" ya podremos ver cómo se comporta nuestra implementación en (casi) todas las situaciones posibles.

Unit Tests (I): “mocking” la interacción

October 6th, 2009
Indispensables. De entre todos los tipos de tests, son los más sencillos de escribir, aunque son los que permiten encontrar (a tiempo) la mayor cantidad de bugs.

¿Qué es un Unit Test? Mejor contar qué hacen y no lo que son: Un Unit Test debe probar un ÚNICO método de una ÚNICA clase. Simple. Sin excepciones (*). El método a probar SIEMPRE debe tener un estado inicial conocido y un resultado esperado. ¡Siempre! Si cumple ambas premisas, el test podrá ser ejecutado de forma automática y sin depender de nadie (**).

Considero que probar la interacción de una clase con el resto de componentes es uno de los sistemas que mejores resultados ofrece. Estas interacciones se "simulan" en el mismo test, utilizando mock-objects.

Antes de empezar con algún ejemplo de Unit Test con Mock Objects, hay algunos consejos que nos pueden facilitar el trabajo:

Para los siguientes ejemplos utilizaré Java 6, JUnit 4.7 y JMock 2.5.1, aunque debería funcionar para cualquier versión Java 5+, JUnit 4+ y JMock 2+.

Utilizando un ejemplo parecido a los de Marc, imaginemos que tenemos que implementar el siguiente método:
public Interface IBombService {

/**
* Check if the country with countryName should be bombed.
*
* @return true if the country should be bombed, or false if not.
*
* @throws NoSuchElementException If no country with the given name exists.
*/
public boolean shouldBeBombed(String countryName) throws NoSuchElementException;

}

Sabemos que el método deberá consultar si el nombre del país es válido. Luego deberá mirar el último ataque en nuestra base de datos, por si es demasiado pronto para volver a ser bombardeado. Finalmente deberá consultar un servicio web externo para verificar si debe ser atacado o no. Nada de HelloWorld... un caso más cercano a "nuestra" realidad.

La estructura que recomiendo usar para cualquier test unitario con JMock es la siguiente:

package cat.edra.impl;

import org.jmock.*;
import org.junit.*;

/**
* Unit test for YetAnotherService methods.
*/
public class YetAnotherServiceTest {

private YetAnotherService yetAnotherService = null;

private Mockery mockery = new Mockery();

// Interfaces de las dependencias del servicio

@Before
public void setUp() {
yetAnotherService = new YetAnotherService();

// Inicialización de cada uno de los mocks de las dependencias
// Inyección de cada una de las dependencias "mocked".

// Mock conditions for every test...
mockery.checking(new Expectations() {
{
// Expectations que deben cumplir TODOS los tests (cache, recuperar un objeto, ...)
}
});
}

@After
public void tearDown() {
mockery.assertIsSatisfied();
}

@Test
public void testYetAnotherServiceCorrectFlow() throws Exception {
mockery.checking(new Expectations() {
{
// Lista de Expectations que debe cumplir este test concreto.
}
});

yetAnotherService.methodToTest(...);
}

@Test(expected = TheExpectedThrownException.class)
public void testYetAnotherServiceIfAnExceptionIsRaised() throws Exception {
[...]

}

En este punto, y aplicando algo de TDD, sería bueno plantearse los casos de prueba. Podríamos empezar implementando algunos de los siguientes:

public void testShouldBeBombedIfCountryNameNotExists()...
public void testShouldBeBombedIfDbException()...
public void testShouldBeBombedIfExternalServiceException()...
public void testShouldBeBombedIfExternalResponseIsTrue()...
public void testShouldBeBombedIfExternalResponseIsFalse()...

El límite está en nuestra imaginación y lo completa que sea nuestra implementación. Utilizar TDD al 100% nos permite implementar SÓLO el código que haga funcionar estos tests. Ni una línea más. Utilizar TDD a medias, nos permitirá ir añadiendo, poco a poco, nuevos casos de tests, aún a riesgo de implementar más líneas de código de las realmente necesarias. Sea como sea, los tests unitarios nos ayudarán (y mucho) a mejorar la calidad de nuestro código.

Como ejemplo, veamos la implementación con mock-objects de uno de los casos correctos y de un caso de error con la base de datos:

    [...]

private BombService bombService = null;

@Before
public void setUp() {
bombService = new BombService();

countryDaoMock = mockery.mock(ICountryDao.class);
bombDaoMock = mockery.mock(IBombDao.class);
bombSoapClientMock = mockery.mock(BombSoapClient.class);

bombService.setCountryDao(countryDaoMock);
bombService.setBombDao(bombDaoMock);
bombService.setBombSoapClient(bombSoapClientMock);
}

@After
public void tearDown() {
mockery.assertIsSatisfied();
}

@Test
public void testShouldBeBombed() throws Exception {
mockery.checking(new Expectations() {
{
exactly(1).of(countryDaoMock).findCountryByName(with("Avalon"));
will(returnValue(new Country("1", "Avalon", "AVA")));

Calendar cal = Calendar.getInstance();
cal.set(2008, 0, 1);
exactly(1).of(bombDaoMock).getLastBombed(
with(any(Country.class)));
will(returnValue(cal.getTime()));

exactly(1).of(bombSoapClientMock).isPendingAttack(
with("Avalon"));
will(returnValue(true));
}
});

boolean result = bombService.shouldBeBombed("Avalon");
assertTrue(result);
}

@Test(expected = NoSuchElementException.class)
public void testShouldBeBombedIfCountryNameNotExists() throws Exception {
mockery.checking(new Expectations() {
{
exactly(1).of(countryDaoMock).findCountryByName(with("Avalon"));
will(returnValue(null));
}
});

bombService.shouldBeBombed("Moon");
}

[...]


Una vez uno se acostumbra a la sintaxis de JMock (o equivalentes), es muy sencillo realizar tests unitarios que validen el comportamiento de nuestro método ante cada excepción y respuesta posible de sus dependencias.

La clase que estamos probando utiliza directamente su implementación private BombService bombService = null; en lugar de su interfaz. Cada test se encarga de reiniciar el estado original del objeto en el método setUp() y validar que todo finaliza correctamente en el tearDown(). El resto de dependencias sólo utilizan su interfaz, que se convertirá en el contrato que deberán seguir las implementaciones para ser compatibles con nuestro código (tanto los casos correctos, como los errores).

La única premisa para implementar estos tests es utilizar (o crear) las interfaces de cada una de las dependencias, con los métodos que pueden ser invocados desde el nuestro. Eso sí, sin implementación, ya que el comportamiento que esperamos de ellas se define en las Expectations.

Llegados a este punto, quedan muchos temas abiertos, que irán apareciendo en próximas entregas:
  • ¿Qué ocurre si alguna de mis dependencias no tiene interfaz?
  • ¿Cómo se prueban capas de acceso a datos o servicios web, que no tienen un estado inicial conocido? 
  • ¿Qué hacer si las Expectations de un test crecen desmesuradamente?
  • ...
Pero esto ya será carne de otros tutoriales... Mientras tanto, let's test! :)

(*) Aunque puedan existir casos justificados de tests unitarios contra más de una clase o método, no es ni normal ni recomendable que abunden en nuestro código.
(**) En próximos posts contaré qué pasa con los tests que no son fácilmente automatizables (como los que acceden a base de datos o sistemas de colas).
(***) Sólo es aconsejable dejar sin interfaz aquellas clases de poca entidad, como algún que otro Helper o similares.
-->