Ir al contenido principal

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>

Comentarios

Entradas populares de este blog

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 HttpClie

Unit Tests (I): "mocking" la interacción

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: Cada clase debe tener su Interfaz (***) , exceptuando las java.lang.Exception y poco más. Conocer el patrón de Inyección de Depen

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(twitte