Thoughts on Software Reflexiones sobre cómo dearrollar software y no morir en el intento

Haciendo pruebas de clases abstractas

El tema que vamos a tratar está relacionado con un tipo especial de testing automático que no suele ser frecuente en nuestro trabajo diario: las pruebas unitarias de clases abstractas. Normalmente somos consumidores de librerías, utilidades y frameworks que nos dan el trabajo ya hecho para que podamos usar sus clases de forma sencilla y sin tener que preocuparnos de nada (o, mejor dicho, de casi nada). En cualquier framework que analicemos, veremos que existe multitud de interfaces y clases abstractas. Nosotros nos limitamos en muchos casos a ir incorporando implementaciones y subclases concretas para que nuestras aplicaciones funcionen. En estos casos, las pruebas unitarias que automatizamos prueban estas implementaciones concretas y se enfrentan a problemas típicos como eliminar las dependencias de componentes externos (ya sea mediante mocks, stubs o algún otro tipo de test double). Pero, ¿qué ocurre cuando somos nosotros los que estamos desarrollando un framework? ¿Cómo podemos probar de forma efectiva nuestro código si éste está plagado de componentes abstractos (ya sean interfaces o clases) y no podemos instanciar las clases más básicas que queremos probar? Algunos pensarán que difícilmente van a tener que desarrollar un framework alguna vez. Sin embargo, esta opción no están tan alejada de nuestras posibilidades. A continuación os presento una forma sencilla de framework que podemos crear nosotros mismos: el patrón Template Method.

Template method

Wikipedia:

es un patrón de diseño enmarcado dentro de los llamados patrones de comportamiento, que se caracteriza por la definición, dentro de una operación de una superclase, de los pasos de un algoritmo, de forma que todos o parte de estos pasos son redefinidos en las subclases herederas de la citada superclase [1].

La estructura básica de clases que le da soporte sería:

Tempate method

Esto nos recordar a otro patrón de diseño parecido que también habla de algoritmos: el patrón Strategy. La diferencia fundamental entre Template Method y Strategy es que, en el primero, las subclases modifican una parte del algoritmo mientras que, en el segundo, las implementaciones de la estrategia cambian el algoritmo completo.

Como podemos ver, el patrón Template Method nos da la base para construir un framework con la estructura básica de los pasos que hay que seguir para realizar una determinada acción, dejando que sean las clases concretas las que completen los detalles necesarios. Veamos ahora cómo lo vamos a aplicar en un ejemplo y de qué forma podemos hacer pruebas unitarias a la clase abstracta que define el método plantilla.

Pongamos un poco de orden

Vamos a hacer un ejemplo muy sencillo donde aplicaremos el patrón Template Method. En concreto, se trata de una clase que define el algoritmo para ordenar una lista de cadenas de caracteres.

Sort algorithm

La clase SortAlgorithm define un algoritmo genérico para la ordenación de una lista de cadenas de caracteres básandose en la implementación concreta del método compare, que se encarga de hacer la comparación básica entre dos cadenas de caracteres cualesquiera. Su implementación en Java sería algo parecido a:

    public List<String> sort(List<String> unsortedList) {
        List<String> sortedList = new ArrayList<String>();
        Iterator<String> it = unsortedList.iterator();
        while(it.hasNext()){
            String element = it.next();
            boolean inserted = false;
            for(int i=0;!inserted && i<sortedList.size();i++){
                if(compare(sortedList.get(i), element)>0){
                    sortedList.add(i, element);
                    inserted = true;
                }
            }
            if(!inserted){
                sortedList.add(element);
            }
        }
        return sortedList;
    }

    protected abstract int compare(String a, String b);

Por supuesto, no vamos a dejar nuestro método de ordenación de listas sin probar (eso no lo hacemos nunca, ¿verdad?). Así que, ¡vamos a crear una buena prueba unitaria!

La opción de testing más sencilla que podría llegar a funcionar

Como no podemos instanciar la clase SortAlgorithm por ser abstracta, lo primero que podríamos hacer para poder probarla unitariamente es proporcionar una subclase de prueba que nos dé una implementación por defecto para el método compare. De esta forma, podríamos hacer:

public class SortAlgormithmConcreteTestSubclass extends SortAlgorithm {

    @Override
    protected int compare(String a, String b) {
        return -1;
    }

}

Con esta implementación del método compare, la cadena a siempre se considerará menor que la cadena b por lo que, con nuestro algoritmo de ordenación genérico, los elementos se irán incorporando a la lista ya ordenada siempre al final. Así tendríamos que el algoritmo de ordenación nos devuelve siempre una lista igual que la primera. El test unitario para el método sort sería el siguiente:

public void testAlgorithm() {
    List<String> aList = new ArrayList<String>();
    aList.add("a");
    aList.add("b");
    aList.add("c");
    aList.add("d");
    aList.add("e");
    SortAlgorithm sortAlg = new SortAlgormithmConcreteTestSubclass();
    List<String> anotherList = sortAlg.sort(aList);
    assertEquals(aList, anotherList);
}

Se trata de una buena implementación de una subclase específica de prueba ya que:

  • La implementación de compare es trivial (una línea de código) y nos devuelve un resultado predecible (útil para la prueba que vamos a programar).
  • Hace que probar el método sort sea sencillo pues sólo tenemos que comprobar que lo que nos devuelve es una lista igual que la original.
  • No sirve realmente para ordenar listas de cadenas de caracteres. Es decir, no estamos dando una implementación concreta del método plantilla que sirva para nada. Su propósito es única y exclusivamente permitirnos hacer pruebas unitarias de nuestro algoritmo con lo que no estaremos nunca tentados de utilizar esta implementación en nuestro código de producción.

Sin embargo también tiene algunos inconvenientes:

  • El test es más difícil de entender ya que, quien lo lee, tiene que buscar en la implementación específica de prueba, SortAlgormithmConcreteTestSubclass, para saber qué hace.
  • Hemos tenido que crear una nueva clase únicamente para poder hacer un test. En nuestro caso, ha sido una clase sencilla pero en otros podría ser mucho más complicada. Como, al final, todo el código que tenemos no es más que inventario, debemos intentar reducirlo al mínimo posible. Esto es más verdad aún para el código de test.

Haciéndolo fácil con Mockito

Este proceso lo podemos simplificar mucho utilizando alguna librería que nos ayude en la tarea de crear un comportamiento predeterminado para el método abstracto compare, de forma que así podamos probar convenientemente nuestro algoritmo de ordenación genérico. Y aquí es donde entra en juego Mockito.

En este caso, vamos a utilizar Mockito para dar comportamiento únicamente al método abstracto compare, mientras que seguimos utilizando toda la lógica ya implementada en el método de ordenación, sort. Nuestro test unitario quedaría ahora así:

    public void testAlgorithm() {
        List<String> aList = new ArrayList<String>();
        aList.add("a");
        aList.add("b");
        aList.add("c");
        aList.add("d");
        aList.add("e");
        SortAlgorithm sortAlg = mock(SortAlgorithm.class, CALLS_REAL_METHODS);
        doReturn(-1).when(sortAlg).compare(anyString(), anyString());
        List<String> anotherList = sortAlg.sort(aList);
        System.out.println(anotherList);
        assertEquals(aList, anotherList);
    }

El detalle fundamental en el que tenemos que prestar atención es el haberle pasado al método mock un parámetro extra: CALLS_REAL_METHODS. Gracias a este parámetro estamos construyendo una implementación de la clase abstracta SortAlgorithm que utiliza los métodos ya definidos en ésta y únicamente proporciona un comportamiento específico cuando se hacen llamadas al método compare con cualquier par de cadenas de caracteres, devolviendo siempre -1 (lo que coincide con la implementación que ya habíamos hecho en SortAlgorithmConcreteTestSubclass, que ya no es necesaria y puede ser deshechada).

De esta forma, podemos hacer pruebas unitarias a nuestras clases abstractas aunque las implementaciones que existan estén en proyectos distintos o, sencillamente, no dispongamos de ellas, y lo podemos hacer con una implementación limpia que nos genera el mínimo código necesario para llevarlas a cabo gracias a Mockito.

Código fuente

Puedes descargar el código fuente completo comprimido en ZIP aquí. O, si lo prefieres, lo puedes descargar del repositorio en Github.

Nota: Al proyecto final se le han incorporado un par de implementaciones que completan el uso del patrón Template Method.