UnitTesting como cultura de desarrollo - del dicho al hecho

En el alt.net open space buenos aires del 11 de diciembre del 2010 @jfroma planteó este tema:

UnitTesting como cultura de desarrollo - del dicho al hecho

Experiencias y desafíos de la evangelización al Unit Testing.

Es muy común escuchar a un desarrollador decir; "sé lo que es Unit Testing, entiendo y aprecio el valor agregado que nos da, sé que es bueno.... pero.... pero no lo hago".

  • ¿Cuáles son los obstáculos con los que nos encontramos?
  • ¿Cómo podemos facilitar el camino en nuestros equipos de desarrollo?
  • ¿es verdad que desarrollar con test requiere más tiempo?

Durante la charla en el open space se mencionaron tres tipos de test: test de aceptación, test de integración y test unitarios. En este blog post me referiré a los dos últimos (integración y unitarios). Algunas de las preguntas planteadas fueron:

  • ¿Cómo empezar a hacer testing?
  • ¿Cómo incluir tests a un proyecto heredado (legacy code)?
  • ¿Cómo convencer a quién no hace uso de esta práctica de comenzar a utilizarla?

¿Cómo empezar a hacer testing?

Pienso que lo primero que tenemos que hacer es entender que ventajas nos da el testing, no se trata de escribir test por el simple hecho de hacer testing y menos aún eso significa que vaya a subir la calidad de nuestro desarrollo de por sí, sino que tenemos que entender que ventajas nos da escribir test y a partir de eso decidir (con fundamentos) si lo queremos hacer o no y que tipo de test (por ejemplo, en varios blog post que he publicado código no escribí ningún test).

A mi forma de ver, una de las ventajas de escribir tests es que la funcionalidad que desarrollamos queda asegurada durante el proceso completo de desarrollo del sistema, con esto me refiero a que si yo u otra persona modifica alguna parte de la aplicación que afecta negativamente la funcionalidad que aseguré, vamos a tener una alerta avisándonos de esta situación (y no vamos a depender de que un tester o el mismo usuario encuentre el problema).

Otra ventaja, cuando uno ya está acostumbrado a escribir tests, es que el desarrollo se vuelve mucho más rápido, no es lo mismo desarrollar una funcionalidad ejecutando y probando “a mano” a que lo haga otro programa por nosotros.

El punto anterior también nos permite que cada vez que desarrollamos algo nuevo o modificamos algo existente estemos probando de no romper ninguna de las funcionalidades aseguradas (y dudo que se haga si no hay testing automatizado).

¿En casa de herrero cuchillo de palo?

Con los mismos fundamentos que, por ejemplo, un banco decide utilizar un sistema para procesar millones de transacciones (velocidad, seguridad en el proceso, reducción del error humano, etc.) es que nosotros podemos elegir utilizar un sistema para probar nuestros desarrollos (velocidad, seguridad en el proceso, reducción del error humano, etc.).

¿Cómo incluir tests a un proyecto heredado (legacy code)?

Ya sea un proyecto heredado o un proyecto en el que trabajamos nosotros pero no hicimos tests creo que el concepto es el mismo, se trata de asegurar funcionalidad.

En estos casos lo más complicado no suele ser pensar los test sino que es muy probable que la aplicación no haya sido pensada en ser testeable, por ejemplo:

  • que no se haya usado inyección de dependencias, lo que va a dificultar emular determinados componentes del sistema para inducir errores, evitar dependencia a los datos, etc.,
  • que la lógica de negocio esté distribuida en distintas capas, incluyendo el frontend,
  • que se dependa de los datos cargados en la base de datos y a su vez que los datos actuales no sean 100% consistentes porque “son datos viejos”,
  • que se usen sistemas externos o dispositivos directamente desde la lógica de negocio sin la opción de emular dichos dispositivos.

Ante este escenario no es imposible escribir test, pero va a ser necesario algunos ajustes y reestructuraciones del código donde destaco dos tipos de test:

  • Test para asegurar funcionalidades existentes
  • Test que aseguren el nuevo trabajo

Test para asegurar funcionalidades existentes

Sobre todo pensando en la funcionalidad crítica, quizás queramos asegurarnos que, por ejemplo, nuestro en sistema de transacciones bancarias nunca dejen de andar las transferencias aunque si dejase de funcionar un reporte de bajas de cuentas podría ser menos crítico (esto no quiere decir que no sea relevante el reporte sino que no va a rodar la cabeza de nadie por que deje de funcionar dicho reporte).

Una forma de hacer test de esto es a caja cerrada (o caja negra), donde probamos la funcionalidad midiendo los resultados y no cómo lo hace, para esto es necesario conocer bien el negocio y no así la codificación (quizás si no se conoce el código es mejor aún). Este tipo de tests se pueden diseñar con los equipos de QA o con los funcionales donde ellos ayuden a pensar los test y los desarrolladores a codificarlos.

Test que aseguren el nuevo trabajo

Ya sean nuevas funcionalidades, modificaciones, corrección de bugs, etc., vamos a querer que los siguientes pasos que se den sean firmes, que no se vuelva a romper lo mismo (por un mínimo de amor propio aunque sea). Para esto vamos a tener que escribir los test y, como dije antes entre las ventajas de tener test, vamos a asegurar la funcionalidad. El equilibro a buscar en esta tarea va a ser cuanto hay que modificar el código existente para que la modificación o nueva funcionalidad sea testeable.

¿Cómo convencer a quién no hace uso de esta práctica de comenzar a utilizarla?

Respecto a este tema, creo que las opiniones de @martinsalias y @carlospeix fueron las más aceptadas y se trata de mostrar resultados y evidenciar cuando la existencia de un test hace evitable un mal momento como por ejemplo “nos evite tener que trabajar un fin de semana”.

Complementos del testing

Todo esto que conversamos, sumado a entornos de integración continua, deploy automatizado para testing ¿todas las noches?, y una buena conducta de: “un issue <-> un commit” creo que da una base sólida para empezar a hablar de calidad en el desarrollo (pero es solo la base).

Links

Blog post de Pedro respecto al open space: http://pedrowood.wordpress.com/2010/12/21/alt-net-open-space-buenos-aires-2010/

Sitio de la comunidad: http://altnethispano.org

Android y WCF REST

image

Este es un ejemplo sobre como conectar una aplicación Android a una servicio WCF REST

El servicio WCF está hosteado en un IIS bajo el nombre de notescenter (para este post) y el cliente desarrollado en Android consume dicho servicio.

El Servicio WCF

Nuestro servicio es muy simple y lo vamos a crear con el template “WCF REST Service Template 40(CS)” ya que el objetivo del ejemplo es mostrar como acceder desde la aplicación Android.

Dicho servicio mantiene una lista de notas en un repositorio en el servidor, si escribimos notescenter.neluz.int/note/help nos va a mostrar las acciones referente al servicio de notas:

image

donde cada acción, siguiendo los principios REST, significa:

  • Acciones sobre http://notecenter.neluz.int/note/
    • GET: obtiene la lista de notas.
    • POST: crea una nueva nota (el contenido se envía en el cuerpo del mensaje).
  • Acciones sobre http://notecenter.neluz.int/note/[id]
    • GET: obtiene la nota cuyo id es [id].
    • PUT: actualiza la nota cuyo id es [id] (el contenido se envía en el cuerpo del mensaje).
    • DELETE: elimina la nota cuyo id es [id]

El código completo del servicio se lo pueden bajar hacienco click aquí.

El cliente Android

Esta aplicación cuenta con 2 layout, uno que nos permite ingresar nuevas notas y otra que lista las notas existentes donde, si seleccionamos una nota la elimina. No hay confirmaciones ni detalles de presentación ya que no es el objetivo del post.

Android-1 Screenshot2

crear una nueva nota

listado de notas

La aplicación va a necesitar permisos de acceso a internet:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

Mientras que la clase que se encarga de la comunicación Http con nuestro servicio WCF hace uso de los objetos HttpGet, HttpPost y HttpDelete. En el caso del POST podemos ver como son serealizados los datos como JSON.

El código es el siguiente:

package com.neluz.messageClient;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
 
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;
 
public class HttpNote {
 
       public static String doGet(String path) throws URISyntaxException, IOException, ClientProtocolException {
               BufferedReader in = null;
               try {
                       HttpClient client = new DefaultHttpClient();
                       HttpGet request = new HttpGet();
                       request.setURI(new URI(path));
                       HttpResponse response = client.execute(request);
 
                       in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
 
                       StringBuffer sb = new StringBuffer("");
                       String line = "";
                       String NL = System.getProperty("line.separator");
                       while ((line = in.readLine()) != null) {
                               sb.append(line + NL);
                       }
                       in.close();
                       String array = sb.toString();
                       return array;
               } finally {
                       if (in != null) {
                               try {
                                       in.close();
                               } catch (IOException e) {
                                       e.printStackTrace();
                               }
                       }
               }
       }
 
       public static String doPost(String path, String text) throws URISyntaxException, JSONException, ClientProtocolException, IOException {
               HttpPost httpost = new HttpPost(new URI(path));
               httpost.setHeader("Accept", "application/json");
               httpost.setHeader("Content-type", "application/json; charset=utf-8");
 
               JSONObject holder = new JSONObject();
               holder.put("Text", text);
 
               StringEntity se = new StringEntity(holder.toString());
 
               httpost.setEntity(se);
 
               DefaultHttpClient httpclient = new DefaultHttpClient();
               HttpResponse response = httpclient.execute(httpost);
 
               StringBuffer sb = new StringBuffer();
 
               BufferedReader in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
 
               String line;
               String NL = System.getProperty("line.separator");
               while ((line = in.readLine()) != null) {
                       sb.append(line + NL);
               }
               in.close();
               return sb.toString();
       }
 
       public static boolean doDelete(String path, int id) throws ClientProtocolException, IOException {
               StringBuilder url = new StringBuilder();
               url.append(path).append(id);
 
               HttpDelete httpdelete = new HttpDelete(url.toString());
 
               DefaultHttpClient httpclient = new DefaultHttpClient();
               HttpResponse response = httpclient.execute(httpdelete);
 
               StatusLine statusLine = response.getStatusLine();
 
               return statusLine.getStatusCode() == 200;
       }
}

y para deserealizar el contenido de los mensajes recibidos usaremos:

package com.neluz.messageClient;
 
import java.util.ArrayList;
import java.util.List;
 
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
 
public class JsonNote {
       public static List<Note> transformJSON2Notes(String response) throws JSONException {
               List<Note> result = new ArrayList<Note>();
 
               JSONArray notes = new JSONArray(response);
               for (int i = 0; i < notes.length(); i++) {
                       JSONObject note = notes.getJSONObject(i);
                       Note n = transformJSON2Note(note);
                       result.add(n);
               }
               return result;
       }
 
       public static Note transformJSON2Note(String note) throws JSONException {
               JSONObject n = new JSONObject(note);
               return transformJSON2Note(n);
       }
 
       private static Note transformJSON2Note(JSONObject note) throws JSONException {
               Note n = new Note();
               n.setId(note.getInt("Id"));
               n.setText(note.getString("Text"));
               return n;
       }
}

el código completo del cliente lo pueden descargar haciendo click aquí.

Nota: estoy trabajando con la versión 7 del API de Android y tuve problemas con la URL, si pongo “http://notecenter.neluz.int/note” al hacer el POST me termina ejecutando el GET, esto se debe a que el IIS cuando recibe la url “http://notecenter.neluz.int/note” devuelve en 307 (Redirect) y el componente de Android hace un GET a la dirección indicada, es decir a “http://notecenter.neluz.int/note/”. Si directamente ponemos “notecenter.neluz.int/note/” en la url del POST funciona perfectamente (notese la diferencia en la / final).