Testeando Javascript Asíncrono: AJAX
Introducción
En el capítulo anterior, repasamos cuál debería ser la estructura básica de una suite de tests, entendiendo por suite el bloque de tests encargado de dar cobertura a un conjunto concreto y relacionado de funcionalidades. Vimos cómo dentro de cada una de estas piezas, que no deja de ser una agrupación abstracta de tests con cierta relación entre ellos, las pruebas se agrupan a su vez en lo que denominamos ‘bloques’. Estos bloques, pueden a su vez anidarse dentro de otros para conseguir una mayor claridad organizativa. Finalmente escribimos algunos ejemplos en los que se mostraba cómo en cada uno, agrupábamos distintos casos de test (esto son las afirmaciones o tests unitarios) que van dando cobertura (y documentación) a las funcionalidades.
En esta nueva entrega analizaremos como atacar el problema del código asíncrono (el que llega a través de una petición AJAX), un importante obstáculo a priori porque se trata de manejar y comprobar algo que aún no tenemos y que, por lo general, depende de un tercero. Para estos casos, recurriremos a los ‘stubs‘ que ya mencionamos en la primera entrega de esta serie y que ahora retomamos para trabajarlos con más profundidad. Vamos allá.
Código Asíncrono
Actualmente, la mayoría de aplicaciones basadas en web hacen uso de peticiones asíncronas al servidor con el fin de establecer una comunicación bidireccional con éste. Su uso más frecuente es mediante los métodos de abstracción que facilita jQuery y que usa, entre otros, Backbone de forma interna para comunicar vistas con modelos y colecciones.
Es por ejemplo habitual encontrar así formularios web que envían los datos al servidor en segundo plano sin refrescar la pantalla, o aplicaciones que cargan el detalle de un producto en una capa al pinchar sobre su miniatura, una galería de imágenes que van añadiendo nuevos lotes a medida que llegamos al final del que estamos viendo, etc… Casi todas estas aplicaciones, como hemos comentado arriba, hacen uso de jQuery como framework para realizar las peticiones gracias a su gran facilidad de manejo. Códigos como este son así vistos una y otra vez:
$.ajax( { type: "POST", url: "some.php", data: { name: "John", location: "Boston" } } ) .done( function( msg ) { console.log( "Data Saved: " + msg ); } ); |
Esta capa funcional de la aplicación también ha de estar cubierta por tests: tenemos que comprobar que la respuesta es correcta, que el formato es el esperado, que los distintos métodos que podamos asociar a la respuesta se disparan correctamente, etc… Sin embargo, el principal obstáculo que encontramos aquí (o diferencia con respecto al código ‘normal’) es que estamos esperando que el servidor envíe los datos que forman parte de nuestra evaluación.
Para enfrentarnos a este problema, recurrimos de nuevo a los ‘stubs’. No obstante, en esta ocasión no basta con interceptar la llamada al servidor anulándola, sino que es necesario además, generar (o emular) una respuesta controlada de éste para que nuestra aplicación continúe su flujo.
Para lograr ese reemplazo, utilizamos el método yieldsTo() proporcionado también por Sinon. El ejemplo más básico de test unitario sobre un código Javacript/jQuery podría ser como sigue:
describe( "Testing a REST API", function () { it( "a GET method should return one item", function () { // Creating the AJAX stub: this.ajax_stub = sinon.stub( $, "ajax" ).yieldsTo( "success", [ { id: 1, title: "Item 1", description: "A beautiful gadget", price: "29.90", stock: "3" } ] ); this.todos = new todoApp.Todos(); this.todos.getItem( 1 ); // Asserts this.todos.should.have.length(1); this.todos.at( 0 ).get( "title" ).should.equal( "Item 1" ); this.todos.at( 0 ).get( "stock" ).should.equal( "3" ); // Restoring this.ajax_stub.restore(); } ); } ); |
En este ejemplo, todo lo interesante ocurre en el método ‘ajax_stub’. Ahí, hemos creado un stub con Sinon para iterceptar el método ‘ajax’ completo de jQuery:
this.ajax_stub = sinon.stub( $, "ajax" ) |
Y, además, hemos forzado una respuesta de tipo ‘success’ gracias al método ‘yieldsTo’ la cual completamos con el objeto que queremos devolver para evaluarlo:
this.ajax_stub = sinon.stub( $, "ajax" ).yieldsTo( "success", [ { id: 1, title: "Item 1", description: "A beautiful gadget", price: "29.90", stock: "3" } ] ); |
Esto quiere decir que cuando se llame al método $.ajax, nuestro stub lo intercepta evitando que jQuery realice la petición y, además, genera una respuesta inmediata de tipo ‘success’ con los datos que hemos introducido en el objeto del ‘yieldsTo’. Con esto, no solo podemos codificar nuestros tests sin necesidad de que las dependencias estén funcionales, sino que podemos incluso ‘provocar’ una respuesta controlada que nos permita testear todos los supuestos necesarios: el objeto que devolvemos puede ser correcto (el esperado), pero podemos también introducir en él algún error para testear cómo se comporta nuestra aplicación cuando lo recibe.
Sobre esto último, pensemos que nuestra aplicación, para el campo ‘price’ que nos llega en la respuesta, está esperando un número con sus decimales separados por un punto (como en el ejemplo dado); sin embargo, por algún error a la hora de meter el valor en la base de datos, alguien introdujo el decimal separado con una coma. Para comprobar cómo nuestra aplicación se enfrenta a ese ‘dato raro’, podemos provocar la respuesta inesperada:
this.ajax_stub = sinon.stub( $, "ajax" ).yieldsTo( "success", [ { ... price: "29,90", ... } ] ); |
De esta forma, podemos ir cubriéndo todos los aspectos relacionados con la lógica en el procesamiento de la respuesta.
La otra ventaja que hemos mencionado solo de pasada conviene repertirla: con esta metodología, no necesitamos que esté funcional la parte de servidor que procesaría la petición AJAX y devuelve la respuesta. Podemos trabajar de forma independiente al equipo de backend, o adelantarnos a que esa parte esté operativa. Esto nos da una gran independencia y acelera el desarrollo al no obligarnos o bien a esperar a que terceros acaben su trabajo o, si todo lo hacemos nosotros, a ir saltando de entorno ‘cliente’ a ‘servidor’ para ir trabajando petición/respuesta de forma simultánea. Cuando somos conscientes de esta ventaja, todo lo que sea volver a la metodología anterior será como darse un paseo por la Edad de Piedra.
Pero el código tiene que ser testeable!
Uno de los puntos importantes de la metodología TDD sobre el que volveremos más adelante es la necesidad de escribir código que sea testeable. Esto es especialmenete delicado en el caso de aquella funcionalidad donde intervengan los métodos jQuery en general y la técnica AJAX en particular.
Con anterioridad a la difusión de los tests unitarios en Javascript, era frecuente abusar de las funciones anónimas como callbacks para los métodos jQuery. Estas funciones permitían agrupar toda una serie de comportamientos de modo compacto a costa de dificultar su reusabilidad posterior dando una falsa sensación de cohesión y estructura. Pensemos por ejemplo en el siguiente código:
$( "#authentication_form" ).on( "submit", function( e ) { e.preventDefault(); var username = $( "#username" ).val(); var password = $( "#password" ).val(); if ( username && password ) { $.ajax( { type: "POST", url: "/authenticate_user", data: { username: username, password: password }, success: function ( data, status, jqXHR ) { if ( data.success ) { $( "#authentication_success" ).show(); } else { $( "#authentication_failure" ).show(); } } } ); } else { $( "#username_password_required" ).show(); } } ); |
Es un ejemplo sencillo de formulario tipo ‘login’ manejado por AJAX: tras pulsar el correspondiente botón, se comprueba el valor de los campos ‘username’ y ‘password’, se envían al servidor para ser autenticados y en función de la respuesta se actúa de uno u otro modo.
Para ir articulando los diferentes comportamientos, se han utlizado funciones anónimas que encontramos con tan frecuencia tanto en nuestros propios desarrollos como en el de terceros. Cierto es que gran culpa de este uso y abuso de las funciones anónimas proviene de la propia documentación de jQuery la cual nos ha acostumbrado mal: tanta anidación da la falsa sensación de que el código queda ‘más recogido’ a costa de volverse completamente ‘no testeable‘.
En nuestro ejemplo anterior, tenemos una de estas funciones anónimas asociada al callback del envío del formulario (evento ‘submit’) y otra asociada a la respuesta AJAX. Con ellas, se crea una clousure (‘clausura’) específica y todo el comportamiento relativo a las mismas parece quedar como comentamos antes bien ‘encapsulado’. Sin embargo, el problema de este planteamiento es que los tests no pueden penetrar en esos métodos para evaluar resultados.
El hecho de que precisamente la funcionalidad se encuentre ‘enterrada’ en las respuestas de otros eventos hace muy difícil llegar a ellas mediante tests unitarios: no están expuestas y por tanto no existe esa API pública que permite al test llamar a un método, pasarle una serie de parámetros y comprobar la respuesta.
Para conseguir que cada pieza de código sea testeable, tenemos que prescindir en la medida de lo posible de las funciones anónimas permitiéndonos esto el interactuar con cada parte por separado. Una solución rápida a este problema conceptual podría afrontarse como sigue:
$( "form" ).on( "submit", submitHandler ); function submitHandler ( e ) { e.preventDefault(); submitForm(); } function submitForm () { /* ... */ this.ajax( { /* ... */ success: done } ); } function done ( data ) { /* ... */ } |
Como vemos, ahora la clave ‘success’ no encierra una función anónima, sino que llama a la función ‘done’ que desarrollamos más abajo. De este modo, cada bloque puede aislarse testearse por separado cuampliéndose así con una de las premisas del TDD.
Conclusión
Testear código asíncrono resulta siempre algo más complejo que el código dado porque hay que interceptar por un lado la petición al servidor y, por otro, facilitar una respuesta emulada de este. Sin embargo, precisamente esto último, el emular la respuesta, nos permite jugar con todas las posibilidades para así comprobar qué ocurre en determinados casos y actuar en consecuencia.
Además de esto, hemos visto que no todo el código es testeable en si mismo: las prácticas habituales a las que podemos estar acostumbrados pueden no ser la forma más eficiente de estructurar una aplicación que ha de estar respaldada por tests. El caso de las funciones anónimas es el más difícil de erradicar. Si aprendemos a extraer todo este código anidado en métodos independientes estaremos consiguiendo por un lado la posibilidad de aplicar tests, pero también por otro, reducir la complejidad ciclomática y facilitar el posterior mantenimiento de esas piezas.
Hay que digerir estos conceptos con calma; así que lo dejamos aquí hasta la próxima entrega. Como siempre, cualquier comentario es bienvenido!
Buenas,
En la función «submitHandler» (en el ejemplo del código separado testeable) se te ha pasado un paréntesis final y veo que en la función «submitForm» el parámetro «done» no es necesario.
La verdad es que ha quedado todo muy clarito, sobre todo el uso de stubs, pero hay una cosa que no entiendo, en la definición de stubs se decía «éstos no tratan de imitar el código original que implementará el objeto». Como puedes desarrollar el resto del código, sin saber los campos que tendrá ese objeto de verdad, es decir usar mocks, no stubs. Creo que los tiros irán por hacer una especie de objeto local, con una función que se encargue de mapear, y tu trabajas con el objeto creado, no con el que te vendrá, asi si mañana cambia, simplemente modificas en el mapeado y punto. Van por ahí los tiros?
Un saludo.
Tienes razón; ya está corregido. Había cambiado mucho código ahí y se me escaparon esos detalles.
En cuanto al stub, es una herramienta muy versátil que tiene varios usos. En este caso del AJAX, la idea es disponer de un objeto falso que se corresponde con los datos que tu aplicación espera o, como lo llamaríamos normalmente, con los datos que devuelve el API al que estás consultando. Puedes no saber los datos (valores) reales que devuelve o devolverá, pero si deberías conocer las claves (los nombres). De esta forma, creas un objeto con esas claves ‘fijas’ y con los valores que te puede interesar recibir: valores correctos para que la aplicación continúe, valores erróneos para manejar las excepciones oportunas, etc…
Si no conocemos absolutamente nada de la respuesta de la API (ni claves ni valores), entonces si deberíamos tener un método intermedio (entre la respuesta de la petición y su evaluación) que mapeara las claves provisionales que hayamos indicado con las que finalmente lleguen. Pero no suele ser un escenario habitual y, si llegamos a eso, es que algo en cuanto a la arquitectura, documentación, especificaciones o comunicación entre las partes está fallando.
Finalmente, el stub es un objeto vivo que puede ‘modificarse’ siempre que sea necesario. Si llegado un tiempo, por cuestiones de diseño o especificaciones, el objeto que te devuelve el API cambia con respecto al original, el stub debe adaptarse al cambio para que con ello, se rompan los tests y pueda reanudarse el ciclo (escribir código, verificar, refactorizar). Algo que puede suceder es que el la respuesta del servidor sea radicalmente diferente, como puede ocurrir por ejemplo al cambiar de API. En este caso, una estrategia válida es la de utilizar un ‘patrón bridge’ que se encargue de recoger los nuevos campos y convertirlos/mapearlos en el modelo antiguo. Este ‘puente’ necesitará también de su correspondiente test, pero no alterará nada de lo que ya teníamos.
De un modo u otro, el objetivo es que los tests permitan adaptarnos a todos los escenarios posibles que pueden darse en nuestra aplicación, reduciendo al máximo el número potencial de errores a la vez que nos permiten identificarlos de una manera clara y prácticamente inmediata.
Saludos!
Pues la verdad es que me tiene algo confundido el mock y el stub. Por lo que comentabas en el primer artículo de la serie TDD moderno, un stub «Como característica propia de los stubs, indicar que éstos no tratan de imitar el código original que implementará el objeto del test, sino únicamente evitar el error que puede causar su indefinición.», con lo que yo entiendo, que no necesita devolver nada, simplemente definirlo. En cambio tu stub de este artículo devuelve una respuesta, que es lo que creia que hacia diferente al mock.
Pensando un poco he llegado a un nuevo enfoque (que la verdad no me convence mucho, pero no se si irán por ahí los tiros): Tal vez el stub sea el caso más inicial con el que uno se puede encontrar, se sabe un poco la estructura que devolverá pero no que tipo de datos contendrán, no es un copia y pega de una respuesta real. Esto te obliga a testear muchas mas cosas que en un mock, que es realmente un copia y pega de una respuesta real de un webservice, con lo que si el campo name ves que es un string no hace falta comprobarlo. Ya digo que me parece un poco, mucho|bastante, extraña esta conclusión que saco y no me convence nada.
Un saludo!
Estoy siguiendo esta serie de articulos nuevamente. Me parece valiosa incluso hoy, el uso del stub y el mock continua generandome duda en lo particular.