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

Drag and drop entre listas con asp.net mvc y jQuery

View

Para hacer la funcionalidad de drag and drop entre listas con jquery basta con una simple instrucción:

$("#left #center #right").sortable({
                connectWith: '.drag-and-drop-connectedSortable'
            }).disableSelection();

Aquí le estamos diciendo que los elementos left, center y right (en mi caso son todas <ul>) son listas conectadas con los elementos que tengan como class a “drag-and-drop-connectedSortable” (que en este caso son las mismas listas), lo que permite tomar elementos de una lista y ponerlos en otra.

Luego agregamos algunos detalles estéticos y logramos algo como:

ui

los nombres en este ejemplo son en agradecimiento a los que participar en la discusión del foro sobre algunos temas relacionados (hasta el momento de la publicación de este post). hilo en altnet-hispano.

Controller

La acción del controller que deseo invocar es la siguiente:

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult SendList(IList<string> left, IList<string> center, IList<string> right)
{
    return Json(new
                    {
                        Right = from i in right select i.ToLower(),
                        Center = from i in center select i.ToLower(),
                        Left = from i in left select i.ToLower()
                    });
}

pero al momento de invocar dicha acción para informarle sobre los elementos seleccionados en cada lista me encontré con algunos inconvenientes que conversamos en altnet-hispano (hilo):

  • Pasar una lista (<ul> <li>) al controller como parámetros de una acción.
  • Armar los parámetros del post para que sean correctamente interpretados por asp.net mvc.
Pasar una lista (<ul> <li>) al controller como parámetros de una acción

Aquí vimos que no era posible que asp.net mvc interpretara la lista como valores que el usuario puede modificar ya que como dijo Fernando solo los <input…> son tenidos en cuenta. Con esto ya decidimos hacer la invocación del POST con jquery.

Armar los parámetros del post para que sean correctamente interpretados por asp.net mvc.

Para esto tuvimos agregar la línea: jQuery.ajaxSettings.traditional = true; ya que sino asp.net mvc no puede interpretar correctamente los parámetros de tipo array.

El código

$(document).ready(function() {
    jQuery.ajaxSettings.traditional = true;

    $("#form").submit(function() {
        var leftvalues = [];
        $("#left li").each(function() { leftvalues.push($(this).text()) });
        var centervalues = [];
        $("#center li").each(function() { centervalues.push($(this).text()) });
        var rightvalues = [];
        $("#right li").each(function() { rightvalues.push($(this).text()) });

        var postData = { left: leftvalues, center: centervalues, right: rightvalues };

        $.post('<%= Url.Action("SendList", "DragAndDrop") %>',
            postData,
            function(data) {
                alert("left: "+ data.Left + "\n"+ "center: "+ data.Center + "\n"+ "right: "+ data.Right);
            },
            'json');

        return false;
    });

Cómo comentaba mas arriba, la primera línea es debido a que estamos usando jQuery 1.4.x por lo que la serealización de parámetros de tipo array es diferente a como la espera asp.net mvc, por eso seteamos el modo tradicional. les dejo un link al respecto: http://forum.jquery.com/topic/jquery-1-4-breaks-asp-net-mvc-parameter-posting

Luego, simplemente estamos tomando los valores de las listas, pasándolos a un array y armando el post.

Código completo

http://dl.dropbox.com/u/7345566/MvcAndjQuery.7z

Proceso asincrónico en asp.net mvc con jquery

Lo que vamos a hacer en esta oportunidad es una aplicación asp.net mvc con un proceso de duración considerable como para querer tener una barra de progreso usando jquery y jquery.ui

Básicamente, esto se logra haciendo llamadas asincrónicas (e independientes) desde la view a las acciones del controller.

mvc

Controller

En el controller vamos a tener dos acciones, una que se va a invocar mediante un POST (el proceso principal) y otra que se va a invocar mediante otro POST y va a devolver un JSON (consulta del estado del proceso).

Nota: Vamos a hacer uso de los items de HttpApplication para guardar el estado del mismo y para poder ejecutar varios procesos en forma simultanea es que los mismos tienen un processId. Otra opción sería, por ejemplo, persistirlo en una base de datos.

Proceso principal
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LongProcess(int loops, string processId)
{
    HttpContext.Application["Status_" + processId] = 0;
    HttpContext.Application["Loops_" + processId] = loops;

    for (int i = 0; i < loops; i++)
    {
        Thread.Sleep(1000); //Action

        HttpContext.Application["Status_" + processId] = i + 1; //Update status
    }
    return View("Result", new ResultModel {Loops = loops});
}

donde Thread.Sleep(1000) está representando la duración de una etapa del proceso.

Consulta del estado del proceso

aquí tomamos los valores del estado del proceso, calculamos el porcentaje y armamos una respuesta JSON.

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult GetStatus(string processId)
{
    int status = HttpContext.Application["Status_" + processId] != null
                    ? (int)HttpContext.Application["Status_" + processId]
                    : 0;
    int loops = HttpContext.Application["Loops_" + processId] != null
                    ? (int)HttpContext.Application["Loops_" + processId]
                    : 0;

    int value = 0;
    if (status != 0 && loops != 0)
        value = status * 100 / loops;

    return Json(new { Result = value });
}

View

La vista propiamente dicha contiene un div con los controles que realizan la invocación:

  • un formulario: fromprocess
  • un hidden para guardar el Id de proceso: processId
  • otro hidden para guardar el Id del intervalo que consulta el estado periódicamente: intervalId
  • una caja de texto para ingresar la cantidad de ciclos del loop que estamos simulando: loops.
  • un botón de tipo submit para invocar al post del formulario.

y otro div para mostrar el progreso, el estado y el resultado.

<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<% using (Html.BeginForm("LongProcess", "Home", FormMethod.Post, new {id = "fromprocess"}))
   {%>
    <div id="invoke">
        <fieldset>
            <%=Html.Hidden("processId", Model.ProcessId, new { id = "processId" })%>
            <input id="intervalId" type="hidden" />
            <%= Html.TextBox("loops", 10, new {id = "loops"}) %>
            <input type="submit" value="Run Long process" />
        </fieldset>
    </div>
    <div id="progress">
        <div id="progressbar"></div>
        <div id="status"></div>
        <div id="result"></div>
    </div>
<% } %>

La función javascript para consultar el estado

Esta función consulta el estado del proceso haciendo un POST e interpretando el JSON de respuesta. En url indicamos el link a consultar, en data los parámetros de la acción (en el controller) y cuando la consulta se completa tomamos el resultado (que ya viene parseado a JSON), asignamos la propiedad Result al progressbar y en texto al div status.

function updateProgress() {
    $.ajax({
        url: '<%=Url.Action("GetStatus", "Home") %>',
        data: { processId: $("#processId").val() },
        success: function(data) {
            if (data != null) {
                $("#progressbar").progressbar("option", "value", data.Result);
                $("#status").html("Progress: " + data.Result + "%");
            }
        },
        error: function(msg) {
            $("#status").html("Error: " + msg);
        }
    });
}
La función javascript para hacer el POST asincrónico

cuando la página se carga estamos inicializando algunas propiedades…

  • en la primera línea estamos configurando el div progressbar como una progressbar de jquery.
  • en la segunda estamos ocultando el div progress (será visible cuando se inicie el proceso).
  • la tercer línea es muy importante para que funcione correctamente en IE, sino IE hace un cache de las llamadas ajax y nunca vuelve a consultar el estado al servidor sino que se queda con la primer respuesta. (para FF y Chrome no es necesaria).
  • en la cuarta línea estamos agregando una función al submit del formulario de nuestra página que va a realizar la misma invocación que haría el formulario pero en forma asincrónica.
$(document).ready(function() {
    $("#progressbar").progressbar({ value: 0 });
    $("#progress").hide();

    $.ajaxSetup({ cache: false }); // this line is needed to ajax on IE

    $("#fromprocess").submit(function() {
        $("#invoke").hide(1000);

        $("#progressbar").progressbar("option", "value", 0);
        $("#status").html("loading...");
        $("#progress").show(1000);

        $.ajax({
            type: "POST",
            url: $("#fromprocess").attr("action"),
            data: $("#fromprocess").serialize(),
            success: function(data) {
                clearInterval($("#intervalId").val());
                $("#progressbar").progressbar("option", "value", 100);
                $("#status").html("complete");
                $("#result").html(data);
            },
            error: function(msg) {
                clearInterval($("#intervalId").val());
                $("#status").html("error");
                $("#result").html(msg);
            }
        });

        var intervalId = setInterval("updateProgress()", 100);
        $("#intervalId").val(intervalId);

        return false;
    });
});

la función que acabamos de poner en el submit hace una invocación ajax de tipo POST a url que tiene configurada como action el form fromprocess, en data envía el formulario serializado, esto hace que no nos quedemos esperando el resultado del post sino que indicamos una función js que se va a invocar cuando el proceso se complete.

Luego seteamos un intervalo de ejecución para la función updateProgress y guardamos el Id del intervalo para, que cuando se complete el proceso, detenerlo.

Cuando se completa el proceso:

  • detenemos la consulta de estado
  • seteamos el progressbar en 100%
  • ponemos el status en complete
  • mostramos el resultado en el div result.

Código completo

para descargar el código completo, click aquí