Introducción
Continuamos con nuestra serie de tutoriales sobre testing moderno para, en esta ocasión, echarle un vistazo a cómo es un bloque de test (también llamado suite) tal y como lo usaremos más adelante. Recordemos que, aunque hemos escogido MOCHA como framework para estas pruebas, la idea es similar para cualquier otro. Del mismo modo, incluso las sintaxis entre bibliotecas no cambia demasiado por lo que, si escogemos otro paquete, únicamente deberíamos revisar su documentación y traducir los siguientes ejemplos.
Metámosnos en materia…
Estructura básica de un bloque de tests
Testear aplicaciones en Backbone es similar a testear cualquier otro tipo de software. Sin embargo, dado que estamos ante una estructura de tipo MVVM (evolución del clásico patrón MVC), es interesante centrarse precisamente en el análisis de los modelos y las vistas.
Al ser estas estructuras partes de un todo, es importante que al arrancar nuestros tests podamos iniciar o acceder a determinados métodos y propiepades ‘compartidas’ que se utilizan durante las pruebas. Estaríamos hablando aquí de por ejemplo instanciar objetos, crear mocks, establecer espías, iniciar alguna variable con un valor que requerimos durante las pruebas, etc…
Para ello, MOCHA, al igual que cualquier otro framework de tests, ofrece dos métodos o funciones dentro de cada bloque (recuérdese que un bloque de tests es aquel que hemos definido mediante el comando ‘describe‘) que se ejecutan antes y después de correr todos los tests unitarios (que definimos anteriormente como funciones ‘it’). Estos métodos son beforeEach() y afterEach(). Veamos un caso de uso típico para pintar después el escenario al que se corresponde:
describe( "Persistence", function () { beforeEach( function() { this.todo = new todoApp.Todo(); this.save_stub = sinon.stub( this.todo, "save" ); } ); afterEach( function () { this.save_stub.restore(); } ); it( "should update server when title is changed", function () { /* ... */ } ); it( "should update server when status is changed", function () { /* ... */ } ); } ); |
El ejemplo anterior es prácticamente un clásico entre los programas de iniciación a los tests. En él, podemos ver un bloque o ‘describe‘ que enmarca a dos tests unitarios o ‘it’s. Además de esto, como se presupone que estos dos últimos actúan sobre un objeto (‘todo‘ en este caso), necesitamos que el mismo esté ‘disponible’ una vez empecemos las comprobaciones. Del mismo modo, y si observamos el título del bloque (‘Persistence‘), podemos intuir que queremos comprobar precisamente la persistencia de ese objeto.
Con estos datos en la mano, queda claro que estamos actuando sobre un modelo de Backbone, o su equivalente en cualquier otro framework.
NOTA: Recordamos llegados a este punto que en cualquier arquitectura de tipo MVC o derivada, un modelo es la capa que se encarga de alterar el estado de un objeto mediante el registro de sus estados. Este comportamiento suele corresponderse con la escritura, lectura o actualización de un dato en una BD u otro sistema de almacenamiento ya sea de forma temporal o, como se especifica en este ejemplo, persistente.
Pensemos por ejemplo que esta persistencia que queremos comprobar exige de una base de datos que guarda el estado de nuestro objeto ‘todo’. Dicha base de datos bien puede no existir aún (dado que en la metodología TDD estamos escribiendo el test antes de implementar nada) o que, pese a que exista, no hay aún una lógica que permita conectarnos a ella para interactuar (una API). Esto que en principio podría parecer prioritario, no lo es bajo un paradigma de tests: podemos aislar cada componente o pieza funcional de modo que podamos avanzar en su desarrollo con independencia del resto. En este caso, recurrimos a lo que anteriormente definimos como un ‘stub‘: nuestra herramienta (Sinon) escuchará las posibles llamadas que realicemos a la API (aún ficticia) de nuestra base de datos para interceptarlas y simular con ello que efectivamente está tanto disponible como en funcionamiento.
Resumiendo los últimos párrafos, nuestros bloque de test requiere de dos condiciones previas para ejecutar sus pruebas unitarias:
- Que exista una instancia del objeto a comprobar (llamado en la terminología de testing ‘SUT‘).
- Que esté disponible (o emulada) una API a través de la cual se desarrolle la lógica de persistencia.
Para cubrir ambos aspectos en el ejemplo anterior se ha añadido el método beforeEach:
beforeEach( function() { this.todo = new todoApp.Todo(); this.save_stub = sinon.stub( this.todo, "save" ); } ); |
NOTA: Dentro de cada bloque, el framework maneja el valor ‘this‘ de forma inteligente de modo que siempre hace referencia al contexto del test independientemente desde donde lo invoquemos.
La primera sentencia crea una instancia del objeto ‘todo‘ a través del constructor que tendremos que definir más adelante; la segunda crea un ‘stub‘ que interceptará cualquier llamada al método ‘save‘ (segundo parámetro de la función) anulándola. Es decir, cuando se trate de ejecutar ‘todo.save()‘, Sinon capturará la llamada no permitiendo que ésta se efectúe y lance un error al no estar disponible. Estamos así ‘controlando’ o ‘redirigiendo’ el flujo real de nuestra aplicación según nos interesa.
Al finalizar los tests unitarios pertinentes, eliminamos nuestro ‘stub‘ anterior devolviendo el control del flujo al programa mediante la función afterEach(). Gracias a esto, cualquier llamada posterior a ese método todo.save() si que lanzaría el correspondiente error de tipo ‘todo.save is not a function‘.
Básicamente este es el esquema o patrón para realizar los tests:
- Se ‘describe’ un bloque que agrupa varios tests unitarios relacionados de alguna forma ‘lógica’.
- Se inician/intancian todas las variables u objetos que podemos precisar durante los tests y que estarán accesibles a todos de forma global.
- Se emulan/interceptan/escuchan todas aquellas funcionalidades que nos interesen durante el transcurso de los tests.
- Escribimos los tests unitarios necesarios.
- Se dejan de emular/interceptar/escuchar todas las funcionalidades marcadas anteriormente para devolver el control normal a la aplicación.
Conclusion
En este breve capítulo, hemos visto la forma de estructurar una cadena de tests utilizando la nomenclatura habitual de este paradigma: suite, describe, before, after, it… Dado que es algo que hay que asumir e interiorizar, no extiendo más la explicación y dejo así tiempo para la reflexión 😀
Como comentamos en la introducción, estos nombre suelen ser comunes a los marcos de tests más habituales por lo que, aunque se está tratando de forma concreta a MOCHA, pueden resultar igual de válidos para otras soluciones similares.
Nos vemos en la siguiente!
Buenas!
Aunque estoy releyendo los artículos para comprender mejor algunos aspectos hecho en falta algo sobre lo cual he encontrado muchas opiniones, pero nada concluyente. Me refiero al uso de métodos privados. ¿Deben haber métodos privados? ¿Todos deben ser públicos? ¿Deben los públicos referir a los privados de forma implícita y detectar por ahí el error, con aparente poco acierto? ¿Puede testearse publicando todos los métodos solo en modo testing?
Por otro lado, a mi personalmente me ocurría que inicialmente no veía la relación del TDD en una aplicación real. Es decir, parece que está muy pensado para hacer un pequeño o gran algoritmo donde solo hay código, pero del de core, pero una web del tipo WebAPP o web normal tiene mas que eso. Estas están plagadas de eventos, de interfaz. Hoy en día comprendo que una cosa es el código que hace las acciones (normalmente las funciones, el core en si), y otra es la parte a través de la cual se ejecutan esas acciones, por ejemplo los típicos bindeos con jQuery en el click o cualquier otra forma de llegar a esas funciones (Parte que por cierto, la de los eventos, supongo que se testea con PhantomJS y cosas similares). A lo que me refiero es que creo que falta explicar un poco el puente de como aplicar TDD a una web normal, por no ir más lejos, por ejemplo este Blog, como se le aplicaría TDD (Si es que acaso tiene sentido hacerlo), por donde se empieza?
Un saludo!
Tus dudas sobre los métodos públicos y privados son un tema que tiene mucha miga. De hecho, el siguiente capítulo de esta serie de tutoriales, el 4, hablará precisamente de esto; concretamente, se centrará en cómo ha de escribirse código testeable ya que, si bien durante la práctica común del lenguaje solemos hacer una clara distinción entre ambos tipos de métodos, cuando hablamos de test ha de prestarse un enfoque especial (y concreto).
Con respecto al uso ‘real’ del TDD, cuando estén las bases teóricas más o menos presentadas, la idea es ver el cómo aplicarlo en una aplicación. De ahí que precisamente haya escogido Backbone como ‘entorno’: al estar hoy día tan extendido, es un buen marco para ir probando cómo se crea una aplicación a partir de pruebas unitarias que los lectores pueden fácilmente reproducir en proyectos propios.
La idea es que veamos las ventajas que puede presentar este tipo de desarrollos frente al tradicional descubriendo que las pruebas no tienen porque limitarse al ámbito de los algoritmos complejos. Dentro de modelos y arquitecturas actuales como son los patrones MVVM, las pruebas pueden asegurarnos el flujo correcto de una aplicación: que la petición de un modelo vía AJAX devuelve efectivamente un modelo, que una vista posee los métodos necesarios antes de renderizarla, que una transacción asíncrona con el servidor no ha provocado una respuesta inesperada en una promesa, etc… Y los tests nos garantizan, ante todo, que el refactorizado del código escrito continúa funcionando como se espera. Ese punto, de importancia capital en un desarrollo, lo explicaré también con calma llegado el momento.
Quizá es lo malo de un tutorial por ‘entregas’; hasta que no esté avanzado, es posible que no se vea todo el conjunto con perspectiva. Lo interesante es, sin embargo, que gracias a los comentarios como el tuyo, no habrá aspectos importantes que queden sin tratar.
Gracias,
saludos!!
Ohh!, GENIAL!!!!!
Personalmente yo también prefiero por partes, veo el criterio (por los motivos comentados en el primero de esta serie) muy acertado. De hecho he escuchado a mucha gente decir esto mismo, que prefieren leer pequeños artículos sobre desarrollo frente a los grandes libros, pequeñas dosis, no solo por el tiempo que supone enfrentarte a un libro, si no por el poder digerir e interiorizar el contenido mejor, por el poder participar en debates en comentarios sobre zonas concretas,… vamos, que perfecto!
Y sin duda muchísimas gracias a ti!! Creo que ni si quiera podría valorarse esta labor altruista que haces, al margen de tu trabajo y vida, sacando tiempo y ayudando a cientos y cientos de personas como yo a comprender mejor el lenguaje, así que GRACIAS!
Saludos!!