Ir al contenido principal

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.

Comentarios

Entradas populares de este blog

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...

¿Introducción?

Hace tiempo que las siglas TDD están de moda. Test Driven Development por todos lados. Pero cuando uno se decide a ponerlo en práctica, lo único que encuentra por Mr. Google son ejemplos sencillos (HelloWorld, PetClinic, el Hospital y sus pacientes, o la última gran incorporación: Twitter en 15 minutos). Por desgracia, cuando un developer decide probar su código, nunca (enfatizo: ¡nunca! ) sirven de nada estos casos tan sencillos. A la hora de la verdad, tenemos varias bases de datos, repositorios de contenidos, conexiones a servicios SOAP (escalofrío), a servicios REST ... Casos difíciles de probar, que agradecen un conjunto de buenas prácticas o ejemplos parecidos. Es muy difícil encontrar información clara sobre por dónde empezar. Sobre dónde buscar. Sobre qué debería hacer un test (o lo que nunca debería hacer). Uno acaba teniendo la impresión que probar código de manera correcta y eficaz es un arte que se aprende con el tiempo, a base de pruebas y (muchos) errores. Cansad...

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.*; im...