Usando MockCursor de Android

Estos dias me he decidido a implementar algunos tests en Android y una de las implementaciones que creo que ha merecido un post ha sido la implementación de un test que ha requerido la personalización de MockCursor, así que aquí está:

Contextualizo un poco:  La idea del test es probar un método que hace uso de un cursor el cual recibe por parámetro.  El método a probar utiliza este cursor para recorrer cada fila resultado y acceder a sus columnas.

El método a probar sería algo como el siguiente (prestad solo atención al uso del cursor)

private BigDecimal calcularTotal(Cursor cursor){
    BigDecimal total = new BigDecimal(0,MathContext.DECIMAL32);
    int unidades = 0;
    float precio = 0;
    if(cursor.moveToFirst()){
        if(DatabaseHelper.TRUE == cursor.getInt(cursor.getColumnIndex(ProductoDbAdapter.KEY_COMPRADO))){
            unidades = cursor.getInt(cursor.getColumnIndex(ProductoDbAdapter.KEY_UNIDADES));
            unidades = unidades <=1? 1:unidades;
            precio = cursor.getFloat(cursor.getColumnIndex(ProductoDbAdapter.KEY_PRECIO));
            if(precio > 0){
                total = total.add(new BigDecimal(unidades*precio));
            }
        }
        while(cursor.moveToNext()){
            if(DatabaseHelper.TRUE == cursor.getInt(cursor.getColumnIndex(ProductoDbAdapter.KEY_COMPRADO))){
                unidades = cursor.getInt(cursor.getColumnIndex(ProductoDbAdapter.KEY_UNIDADES));
                unidades = unidades <=1? 1:unidades;
                precio = cursor.getFloat(cursor.getColumnIndex(ProductoDbAdapter.KEY_PRECIO));
                if(precio > 0){
                    total = total.add(new BigDecimal(unidades*precio));
                }
            }
        }
    }
    return total;
}//Fin calcularTotal

Para testear este método de forma unitaria, es decir, aislada del resto, necesitamos que sea independiente del Cursor, y ahí es donde entra MockCursor para echarnos un cable. He de decir que MockCursor no es un JMock,  solo es una clase que implementa la interfaz  Cursor devolviendo UnsupportedOperationException para cada llamada de sus métodos., por lo tanto para usarlo hay que sobreescribir los métodos que vamos a usar.

Otro punto a destacar es que el método a probar es privado y no podremos probarlo tal cual. Mas adelante veremos como.

Bien, para probar nuestro método debemos personalizar el MockCursor, es decir, extenderlo y personalizar los métodos que queremos usar. Hay varias formas de hacerlo, una clase anónima, una clase anidada, etc. El lector que decida que solución es la que mas le convence.  Yo usaré una clase anidada al test. La idea de la implementación es simular el recorrido por los resultados obtenidos por una supuesta consulta y devolver los valores cuando se acceda a las columnas de cada resultado. El método a probar solo accede a tres columnas, así que no es demasiado complicado.

public class MockCursorAdapted extends MockCursor{
        int actualIndex = 0; //Indice para el recorrido por los resultados
        Map<Integer,String> entry;
        List<Map<Integer,String>> entryList;
        Map <String,Integer> columnIndexes;
        //Value initialization
        {
            //Column indexes: Se configuran las posiciones de las columnas en función de sus nombres.
            columnIndexes = new HashMap<String,Integer>();
            columnIndexes.put(ProductoDbAdapter.KEY_UNIDADES, 0);
            columnIndexes.put(ProductoDbAdapter.KEY_PRECIO, 1);
            columnIndexes.put(ProductoDbAdapter.KEY_COMPRADO, 2);
            //Creación de los resultados. Se crea una lista de mapas. Cada entrada en la lista es
            //un resultado. Y cada resultado, es un mapa con los valores de cada columna.  
            entryList = new ArrayList<Map<Integer,String>>();

            Map<Integer,String> m = new HashMap<Integer,String>();
            m.put(getColumnIndex(ProductoDbAdapter.KEY_UNIDADES),"1");
            m.put(getColumnIndex(ProductoDbAdapter.KEY_PRECIO),"22.40");
            m.put(getColumnIndex(ProductoDbAdapter.KEY_COMPRADO),"1");
            entryList.add(m);

            m = new HashMap<Integer,String>();
            m.put(getColumnIndex(ProductoDbAdapter.KEY_UNIDADES),"2");
            m.put(getColumnIndex(ProductoDbAdapter.KEY_PRECIO),"5.50");
            m.put(getColumnIndex(ProductoDbAdapter.KEY_COMPRADO),"1");
            entryList.add(m);

            m = new HashMap<Integer,String>();
            m.put(getColumnIndex(ProductoDbAdapter.KEY_UNIDADES),"1");
            m.put(getColumnIndex(ProductoDbAdapter.KEY_PRECIO),"100.0");
            m.put(getColumnIndex(ProductoDbAdapter.KEY_COMPRADO),"1");
            entryList.add(m);
        }

        @Override
        public String getString(int columnIndex) {
            return getValue(columnIndex);
        }

        @Override
        public float getFloat(int columnIndex) {
            return Float.parseFloat(getValue(columnIndex));
        }

        @Override
        public int getInt(int columnIndex) {
            return Integer.parseInt(getValue(columnIndex));
        }

        /**
         * Obtención del valor de la columna indicada para la fila actual
         * @param columnIndex
         * @return
         */
        private String getValue(int columnIndex){
            entry = entryList.get(actualIndex);
            String value = entry.get(columnIndex);
            if(value == null){
                Log.e(TAG, "Sin valor para columna "+columnIndex+" y actualIndex "+actualIndex+" entrada "+entry.toString());
            }
            return value;
        }

        @Override
        public int getColumnIndex(String columnName) {
            return columnIndexes.get(columnName);
        }

        @Override
        public boolean moveToFirst() {
            return true;
        }

        @Override
        public boolean moveToNext() {
            // TODO Auto-generated method stub
            return ++actualIndex < entryList.size();
        }
    }//FIN MockCursorAdapted

Bien, ya tenemos el Mock del cursor personalizado. Creando la clase que ejecutará el test ya lo tendriamos resuelto… bueno… falta el acceso al método privado…

Bien, la forma de ejecutar el método privado es mediante reflexión, digamos que le pedimos a la clase que nos de el método que tiene dicho nombre. De forma muy resumida y sin controlar excepciones:

 Cursor cursorAdapted = new MockCursorAdapted();
 ActividadConMetodoCalcularTotal mActivity = new ActividadConMetodoCalcularTotal();
 Method m = this.mActivity.getClass().getDeclaredMethod("calcularTotal", new Class[]{Cursor.class});
 m.setAccessible(true); //Le decimos que nos deje ejecutarlo desde fuera 😛
 BigDecimal resultado = (BigDecimal) m.invoke(mActivity , new Object[]{cursorAdapted});

Listo!

Un apunte mas:

OJO! MockCursor está disponible a partir de la API 8, por lo que debeis ejecutar los test en un emulador con la versión correspondiente (creo que a partir de la 2.3.3). Si no lo haceis, dará un error en ejecución de NoClassDefFoundError sobre la clase personalizada (en este caso $MockCursorAdapted). Mirando mas detenidamente las trazas se puede ver al principio un warning “unable to resolve superclass” refiriendose a MockCursor… este error me ha tenido ocupado un buen rato.

Espero que os haya gustado la aportación, suerte con los test!