sábado, 5 de noviembre de 2011

Robolectric (I): Unit Testing en Android

Al intentar aprender Android y cómo desarrollar software sobre su plataforma, lo primero que sorprende es lo complicado (y lento) que resulta hacer TDD. Por suerte, la gente de Pivotal Labs ha creado Robolectric, un framework que permite realizar Unit Tests sin disponer de un emulador Android activo.

En primer lugar, debes disponer de un proyecto Android, generado desde Eclipse o desde Línea de Comandos.

A partir del proyecto, y usando Maven, la instalación es inmediata. Simplemente hay que crear un fichero pom.xml en la raíz del proyecto, con las siguientes dependencias:

[... Cabecera del Proyecto ...]

<dependencies>
    <dependency>
        <groupId>com.google.android</groupId>
        <artifactId>android</artifactId>
        <version>2.3.3</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>com.pivotallabs</groupId>
        <artifactId>robolectric</artifactId>
        <version>1.0</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.8.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

[... Más etapas y plugins del proyecto ...]

Como es normal, nuestro proyecto Android debe estar estructurado siguiendo las guías de Maven, con el código de la aplicación dentro de src/main/java y el código de nuestros tests en src/test/java. En el caso de los recursos gráficos (strings.xml, layouts, imágenes, ...), estos deben mantenerse en la carpeta res, y no en src/main/resources, respetando la disposición de contenidos de Android). Para más información sobre cómo configurar Eclipse y Maven, el siguiente artículo es tremendamente útil y completo: http://unbeagleyyo.wordpress.com/2011/03/12/empezando-con-android-maven-robolectric-y-roboguice

Aparte de esto, sólo hay que tener en cuenta que la dependencia android debe tener un scope provided, y robolectric lo debe tener como test. Nuestros tests tendrán que ejecutarse con el Test Runner RobolectricTestRunner.

Y ahora ya podemos comenzar a realizar TDD. En este primer ejemplo, veamos cómo probar el flujo de ejecución entre actividades:

import static com.xtremelabs.robolectric.Robolectric.*;
import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import android.content.Intent;
import android.widget.Button;

import com.xtremelabs.robolectric.RobolectricTestRunner;
import com.xtremelabs.robolectric.shadows.ShadowActivity;

@RunWith(RobolectricTestRunner.class)
public class HomeActivityTest {

    private HomeActivity activity;

    @Before
    public void setUp() throws Exception {
        activity = new HomeActivity();
    }

    @Test
    public void testButtonShouldOpenSecondActivity() {
        activity.onCreate(null);

        Button button = (Button) activity.findViewById(R.id.button);
        clickOn(button);

        ShadowActivity shadowActivity = shadowOf(activity);
        Intent next = shadowActivity.getNextStartedActivity();
        assertNotNull(next);
        assertEquals(SecondActivity.class.getName(), next.getComponent().getClassName());
    }

}

En este primer ejemplo se instancia una Actividad inicial (HomeActivity). Al presionar el botón que contiene, comprobamos que el control lo obtiene una segunda actividad (SecondActivity).

Para conseguir nuestro propósito, Robolectric nos ofrece una Actividad especial llamada ShadowActivity, que publica una gran cantidad de métodos que normalmente no están disponibles en la clase Activity. En este caso, se está usando el método getNextStartedActivity() que retorna la siguiente actividad añadida a la pila de actividades del proceso.

Gracias a Robolectric podremos probar prácticamente cualquier situación de nuestras Actividades y Servicios de forma aislada. En los próximos posts iré mostrando las situaciones que he ido encontrando a medida que profundizaba en el desarrollo sobre Android.



Nota 1: Si después de realizar estos pasos aparece el error "error inflating layout/main" puede ser debido a que Robolectric no encuentra el API de Android o que está usando una versión incorrecta. A mi me funcionó añadir la siguiente línea en el fichero AndroidManifest.xml:
<uses-sdk android:minSdkVersion="8" />

Nota 2: Si aparece un error ClassNotFoundException de la clase R, el motivo es que Maven y/o Eclipse no tienen la carpeta gen incluida. Aunque hay varias soluciones potentes (como maven-android-plugin), una solución temporal es añadir al fichero pom.xml el build-helper-maven-plugin:
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.4</version>
    <executions>
        <execution>
            <id>default</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>add-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>${basedir}/gen</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

martes, 28 de diciembre de 2010

Dependency Injection de HttpClient 3.x

Para un fanático de la Dependency Injection como yo, siempre me ha costado trabajar con Apache HttpClient 3.x y la multitud de maneras que tiene la gente de inicializar los clientes. Además, como usuario asiduo de Spring Framework, es imprescindible disponer de una manera de cargar clientes HTTP desde el contexto XML y que sea sencilla de probar.

Una de las soluciones más comunes para crear y configurar los clientes es la declaración de un método init() donde se inicializan todos los parámetros, obligando que nuestra clase conozca todos los detalles y parámetros de este componente externo. Pese a que la solución funciona, no deja de ser complicada de testear, y esto es justamente lo que se quiere evitar desde el Test Driven Development (TDD). Para ello, hay que buscar una fórmula que permita "inyectar" los clientes HTTP, ya configurados en el exterior, permitiendo a la clase de destino centrarse en su objetivo.

En primer lugar, hay que conseguir la biblioteca HttpClient 3.1, usando nuestro gestor de dependencias favorito: http://mvnrepository.com/artifact/commons-httpclient/commons-httpclient/3.1. Al finalizar el proceso, deberíamos tener descargadas las siguientes dependencias:

El siguiente paso es declarar una instancia de HttpClient, sin inicializar, en la clase que necesite realizar conexiones web. Además tendremos que decidir cómo se configurará la instancia desde el exterior: por inyección en el constructor, por inyección usando un método setter o utilizando algún sistema por anotaciones, como el @Autowired de Spring Framework:

import org.apache.commons.httpclient.HttpClient;

public class HttpClient3Injected {

  @Autowired // Dependency injection by annotation.
  private HttpClient client = null;

  /**
   * Dependency injection by constructor.
   */
  public HttpClient3Injected(HttpClient client) {
    this.client = client;
  }

  [...]

  /**
   * Dependency injection by setter.
   */
  public void setClient(HttpClient client) {
    this.client = client;
  }

}

Sólo con esta declaración, ya podemos realizar tests unitarios con mocks, que nos permitan simular cualquier entrada y salida del HttpClient sin tener que realizar la petición real. Por ejemplo, usando JMock 2.5.x:

import static org.junit.Assert.assertNull;

import org.apache.commons.httpclient.*;
import org.jmock.*;
import org.junit.*;
import org.jmock.lib.legacy.ClassImposteriser;

public class TestHttpClient3Injected {

  private HttpClient3Injected example = null;

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

  private HttpClient clientMock = null;

  @Before
  public void setUp() throws Exception {
    clientMock = mockery.mock(HttpClient.class);

    example = new HttpClient3Injected(clientMock);
  }

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

  @Test
  public void testConnectShouldReturnNullIfHttpConnectionFails() throws Exception {
    mockery.checking(new Expectations() {
      {
        exactly(1).of(clientMock).executeMethod(with(any(HttpMethod.class)));
        will(throwException(new HttpException("Oops!")));
      }
    });

    assertNull(example.connect());
  }

}

Ya podemos definir todos los casos de test que necesitemos e implementar el método connect usando TDD.

Pero ahora queda ver cómo utilizar esta clase desde el exterior. Es tan sencillo como crear una instancia de HttpClient, con la configuración que necesitemos, y facilitarla a nuestra clase:

// Simple default HttpClient
HttpClient client = new HttpClient();
HttpClient3Injected myInstance = new HttpClient3Injected(client);

// HttpClient with a multithreaded customized connection pool
HttpConnectionManagerParams mgrParams = new HttpConnectionManagerParams();
mgrParams.setDefaultMaxConnectionsPerHost(20);
mgrParams.setMaxTotalConnections(100);
mgrParams.setSoTimeout(300000);
MultiThreadedHttpConnectionManager manager = new MultiThreadedHttpConnectionManager();
manager.setParams(mgrParams);
HttpClient client = new HttpClient(manager);
HttpClient3Injected myInstance = new HttpClient3Injected(client);

Y lo mejor de todo, gracias a este método podemos inyectar un HttpClient totalmente configurado desde XML a nuestra clase, usando un framework de inyección de dependencias, como Spring Framework:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="instance" class="cat.edra.web.HttpClient3Injected">
    <constructor-arg ref="client" />
  </bean>

  <bean id="client" class="org.apache.commons.httpclient.HttpClient">
    <constructor-arg ref="manager" />
  </bean>

  <bean name="manager" class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager">
    <property name="params">
      <bean class="org.apache.commons.httpclient.params.HttpConnectionManagerParams">
        <property name="defaultMaxConnectionsPerHost" value="20" />
        <property name="maxTotalConnections" value="100" />
        <property name="soTimeout" value="300000" />
      </bean>
    </property>
  </bean>

</beans>

Como véis, este método permite centrarse en la implementación de nuestra clase, y siempre precedida de una batería completa de tests. La inyección de dependencias nos ofrece un HttpClient completamente configurado por la clase que nos invoque.

viernes, 13 de agosto de 2010

"Mocking the Time": Probando métodos que dependen de la fecha

En todos los proyectos aparecen métodos que necesitan realizar comprobaciones de fechas u horas en la lógica de negocio, ya sea consultando la hora actual del sistema, ya sea comparando rangos horarios.

Si antes de implementar el método le damos un pensada a cómo probarlo, aparece una duda importante: ¿cómo puedo suplantar la fecha o la hora para que mis tests no dependan del día o la hora actuales?

Pongamos un ejemplo sencillo: tenemos un método que si lo ejecutamos en un día par, tiene que llamar a un servicio externo. Una primera implementación, sin pensar cómo probarlo, nos daría un código parecido a:

[...]

public class TimeService {

    private IExternalService externalService = null;

    public void timeMethod() {
        Calendar cal = Calendar.getInstance();
        if (cal.get(Calendar.DAY_OF_MONTH) % 2 == 0) {
            externalService.externalMethod();
        }
    }

    [...]
}

Un código sencillo... ¡que resulta imposible de probar correctamente! Como se puede ver a continuación, si el test se ejecuta en un día par, funcionará. Pero si el test se ejecuta un día impar (situación corriente en un sistema de integración continua), fallará:

[...]

public class TimeServiceTest {
    
    [...]

    // ¡¡Este test sólo funciona los días pares!!
    @Test
    public void testTimeServiceShouldCallExternalServiceIfDayIsEven() {
        mockery.checking(new Expectations() {
            {
                exactly(1).of(externalServiceMock).externalMethod();
            }
        });

        timeService.timeMethod();
    }

    [...]

}

Llegados a este punto, a uno se le pueden ocurrir soluciones del estilo: "cambiemos la fecha del sistema"... pero no son muy recomendables. Yo prefiero aprovechar las bondades de la inyección de dependencias, que nos facilitará enormemente la tarea de probar el código.

Una posible solución es crear una clase TimeResolver (y opcionalmente su interfaz ITimeResolver) y utilizarla desde nuestros servicios para consultar la fecha actual:

package utils;

import java.util.Calendar;
import java.util.Date;

public class TimeResolver implements ITimeResolver {

    @Override
    public Calendar getCurrentCalendar() {
        return Calendar.getInstance();
    }

    @Override
    public Date getCurrentDate() {
        return new Date();
    }

}

Gracias a esta ayuda, ya podemos crear un test que simule lo que sucede en cualquier combinación de fechas y horas:

[...]

public class TimeServiceTest {

    private TimeService timeService = null;

    private Mockery mockery = new Mockery();
    private IExternalService externalServiceMock = null;
    private ITimeResolver timeResolverMock = null;

    @Before
    public void setUp() throws Exception {
        timeService = new TimeService();

        externalServiceMock = mockery.mock(IExternalService.class);
        timeResolverMock = mockery.mock(ITimeResolver.class);

        timeService.setExternalService(externalServiceMock);
        timeService.setTimeResolver(timeResolverMock);
    }

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

    @Test
    public void testTimeServiceShouldCallExternalServiceIfDayIsEven() {
        mockery.checking(new Expectations() {
            {
                Calendar cal = Calendar.getInstance();
                cal.set(Calendar.DAY_OF_MONTH, 8); // Even Day
                exactly(1).of(timeResolverMock).getCurrentCalendar();
                will(returnValue(cal));

                exactly(1).of(externalServiceMock).externalMethod();
            }
        });

        timeService.timeMethod();
    }

    @Test
    public void testTimeServiceShouldNotCallExternalServiceIfDayIsOdd() {
        mockery.checking(new Expectations() {
            {
                Calendar cal = Calendar.getInstance();
                cal.set(Calendar.DAY_OF_MONTH, 7); // Odd Day
                exactly(1).of(timeResolverMock).getCurrentCalendar();
                will(returnValue(cal));

                // External Method will not be called
                exactly(0).of(externalServiceMock).externalMethod();
            }
        });

        timeService.timeMethod();
    }

}

Este par de casos de test servirán para validar que el nuevo código se comporta como esperamos:

[...]

public class TimeService {

    private IExternalService externalService;
    private ITimeResolver timeResolver;

    public void timeMethod() {
        Calendar cal = timeResolver.getCurrentCalendar();
        if (cal.get(Calendar.DAY_OF_MONTH) % 2 == 0) {
            externalService.externalMethod();
        }
    }

    [...]

}

¡Conseguido! Ya podemos asegurar el correcto comportamiento de todos los casos que queramos. Sin duda, esta pequeña indirección me ha sido muy útil siempre que he necesitado trabajar con fechas o calendarios. Espero que os sea de utilidad.

domingo, 18 de julio de 2010

Unit Testing & jMock: expectations comunes

Llega un momento en que algunos de nuestros juegos de prueba empiezan a tener muchas expectations y se empieza a complicar la implementación de nuevos tests o el mantenimiento de los existentes.

Una de las consecuencias de aplicar TDD es que cuánto mayor sea la complejidad del test, mayor será la complejidad de nuestro código, y por lo tanto, de su mantenimiento. Así que si empezamos a tener tests con demasiadas expectations, es buen momento para plantearse el refactoring de alguna de las clases, para simplificar o dividir su funcionalidad.

Si aún así seguimos mantiendo unas expectations muy largas, y que además se comparten entre varios tests, es muy recomendable agruparlas. Gracias a la implementación de jMock, podemos declarar varios bloques de expectations en un mismo test, de tal modo que la prueba deberá cumplir las condiciones de todos sus bloques:

@Test
public void testFindHouseShouldReturnOneHouse() throws Exception {
    mockery.checking(new Expectations() {
        {
            // Expectations #1
        }
    });

    mockery.checking(new Expectations() {
        {
            // Expectations #2
        }
    });

    // ... code of the test ...
}

Gracias a esta característica de jMock, podremos refactorizar y extraer el código común en varios métodos private, dentro de nuestra clase de test. Cada uno de estos métodos contendrá uno (o varios) de los bloques de expectations compartidas. Por ejemplo:

@Test
public void testFindHousesShouldReturnHousesThatMatchColor() throws Exception {

    declareCommonExpectations(); // Expectations comunes

    mockery.checking(new Expectations() {
        {
            // Expectations of this test
        }
    });

    // ... code of the test ...
}

@Test
public void testFindHousesShouldReturnHousesThatMatchLocation() throws Exception {

    declareCommonExpectations(); // Expectations comunes

    mockery.checking(new Expectations() {
        {
            // Expectations of this test
        }
    });

    // ... code of the test ...
}

[...]

private void declareCommonExpectations() throws Exception {

    mockery.checking(new Expectations() {
        {
            // Este bloque contiene todas las expectations
            // comunes que compartirán ambos tests.
        }
    });
}

Este tipo de refactor para agrupar código común nos permitirá reducir bastante la longitud de nuestras clases de test, permitiendo que nos centremos en lo realmente importante: cuál es la entrada esperada, y cuál debe ser la salida. Sin duda, resulta especialmente útil cuando nuestra test suite está repleta de métodos findX() o updateY(), cuyo resultado queremos que sea el mismo en varias de las pruebas.

lunes, 5 de abril de 2010

Matchers y jMock

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:

[...]

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.

domingo, 22 de noviembre de 2009

Unit-Testing SOAP: ¿Qué hacemos con los Holder?

En los últimos meses he tenido que integrarme con algunos Servicios Web (SOAP). Suerte que frameworks como CXF consiguen que la tarea sea algo más sencilla de lo que era hace unos años...

El problema vino cuando intenté probar uno de mis métodos: las clases que necesitaba llamar (auto-generadas con CXF) tenían como parámetros instancias de Holder (parámetro de entrada/salida). Y yo necesitaba utilizar los resultados de los Holder para continuar...

Veamos un ejemplo. Mi código tiene que llamar a esta interfaz:

package external.soap;

import javax.xml.ws.Holder;

// Omito todas las anotaciones y demás meta-información...
public interface IWebService {
    public String execute(int param, Holder<String> innerResult);
}

E imaginemos que tiene que hacer algo tan sencillo como concatenar la salida del método, con el resultado contenido en innerResult. El test que lo comprobara podría ser tan sencillo como:

[...]
import org.jmock.*;
import org.junit.*;
import static org.junit.Assert.*;

import javax.xml.ws.Holder;
import external.soap.IWebService;
    
public class MyServiceTest {
    
    private MyService myService = null;

    private Mockery mockery = new Mockery();
    private IWebService webServiceMock = null;

    @Before
    public void setUp() {
        myService = new MyService();

        webServiceMock = mockery.mock(IWebService.class);
        myService.setWebService(webServiceMock);
    }

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

    @Test
    public void testBothResultsAreConcatenated() throws Exception {
        mockery.checking(new Expectations() {
          {
            exactly(1).of(webServiceMock).execute(with(any(Integer.class)),
                    with(any(Holder.class)));
            will(returnValue("outer"));
          }
        });

        String result = myService.executeOperation(1);
        assertEquals("outer-inner", result);
    }
}

Por muchos "inventos" que hagamos en el método executeOperation(), no habrá manera de hacer funcionar ningún test que tenga Holders, ya que del mismo modo que especificamos que el método IWebService.execute() tiene que retornar la respuesta "outer", no estamos declarando en ningún sitio que además debería rellenar el Holder con la respuesta "inner".

¿Se puede hacer algo al respecto? Aunque el capítulo 15 del JMock Cookbook explique la manera de realizar acciones concretas dentro de los objetos mock utilizando BeanShell, creo que es una solución demasiado compleja. Sin embargo, sí que es posible encontrar una solución sencilla y fácil de implementar para estas situaciones: extender las Action. Utilizando la propia jerarquía de clases de JMock, se puede redefinir el comportamiento de un objeto, implementando la clase Action.

A la hora de la verdad, yo prefiero utilizar la subclase VoidAction, creando una clase anónima y redefiniendo su método invoke:

[...]
import org.jmock.*;
import org.junit.*;
import static org.junit.Assert.*;

import javax.xml.ws.Holder;
import external.soap.IWebService;
    
public class MyServiceTest {
    
    private MyService myService = null;

    private Mockery mockery = new Mockery();
    private IWebService webServiceMock = null;

    @Before
    public void setUp() {
        myService = new MyService();

        webServiceMock = mockery.mock(IWebService.class);
        myService.setWebService(webServiceMock);
    }

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

    @Test
    public void testBothResultsAreConcatenated() throws Exception {
        mockery.checking(new Expectations() {
          {
            exactly(1).of(webServiceMock).execute(with(any(Integer.class)),
                    with(any(Holder.class)));
            will(new VoidAction() {
                @Override
                public Object invoke(Invocation invocation) throws Throwable {
                    Holder<String> holder = (Holder) invocation.getParameter(1);
                    holder.value = "inner";
                    return "outer";
                }
            });
          }
        });

        String result = myService.executeOperation(1);
        assertEquals("outer-inner", result);
    }
}

Como se puede ver, aunque la sintaxis es algo extraña, se consigue definir el comportamiento exacto de cada uno de los parámetros de entrada/salida. Sólo hay que tener en cuenta que el primer parámetro es invocation.getParameter(0), el segundo es invocation.getParameter(1), y así sucesivamente.

Por último, si vemos que tenemos que probar muchas veces el mismo comportamiento, y se nos repite en varios tests, siempre podemos optar por crear una inner class dentro de nuestra clase de pruebas, creando una nueva instancia desde cada uno de los tests:

    [...] 

    @Test
    public void testBothResultsAreConcatenated() throws Exception {
        mockery.checking(new Expectations() {
          {
            exactly(1).of(webServiceMock).execute(with(any(Integer.class)),
                    with(any(Holder.class)));
            will(new OuterInnerAction("outer", "inner"));
          }
        });

        String result = myService.executeOperation(1);
        assertEquals("outer-inner", result);
    }

    // ... other tests.

    private class OuterInnerAction extends VoidAction {
        private String returnValue = null;
        private String holderValue = null;

        public OuterInnerAction(String returnValue, String holderValue) {
            this.returnValue = returnValue;
            this.holderValue = holderValue;
        }

        @Override
        public Object invoke(Invocation invocation) throws Throwable {
            HolderHolder<String> holder = (Holder) invocation.getParameter(1);
            holder.value = holderValue;
            return returnValue;
        }
    }

    [...]

Aunque la solución no sea tan elegante como utilizar BeanShell, creo que es una manera rápida y sencilla de dotar a cada test de un comportamiento "a medida".

¡Suerte!

sábado, 17 de octubre de 2009

Unit Tests (II): clases sin interfaz

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.