Tests unitarios, de integración y de aceptación en Angular con Jasmine, Karma y Protractor

En este tutorial vamos a hablar de un tema que como desarrolladores deberíamos tener presente en cualquier tecnología que estemos utilizando para implementar y validar nuestras soluciones: los tests.

Índice de contenidos


1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Mac Book Pro 15″ (2,3 Ghz Intel Core i7, 16 GB DDR3)
  • Sistema Operativo: Mac OS Sierra
  • VSCode 1.12.2
  • @angular/cli 1.0.6
  • jasmine 2.5.2
  • protrator 5.1.0
  • karma 1.4.1

2. Introducción

Este tutorial parte como explicación en texto de lo que se vio en el taller de testing de NgLabs en el marco del Meetup de Angular Madrid y donde Jorge Baumann (@baumannzone) grabó un screencast con los pasos que dimos en el taller para la implementación de los tests. Que podéis seguir en este enlace: Taller de testing con Angular.

Cuando hablas con desarrolladores te das cuenta de que muy pocos saben y hacen tests de sus implementaciones. Entonces, les pregunto ¿cómo me demuestras que lo que has hecho está bien? Siempre me suelen contestar “pues mira abro la aplicación, pincho en el botón y sale lo que tiene que salir”, me dicen orgullosos. Entonces es cuando les digo y si te pido que esto me lo demuestres cada vez que hagas un cambio en el código junto con las otras mil historias que has implementado para saber que esto se puede poner en producción… entonces es cuando algunos cambian el rictus y otros, los más atrevidos, dicen “bueno pero esto ya lo validará el equipo de QA que para eso está”.

Y ese, amigos, es uno de los mayores problemas de las grandes compañías que tienen un equipo de QA, que encima no automatiza las pruebas con lo que cual el tiempo desde que una persona de negocio tiene una superidea, que va a reportar millones a su empresa, es directamente proporcional al tiempo que el equipo de QA, con sus pruebas en Excel supercurradas, va a tardar en validar ese desarrollo para su puesta en producción; perdiendo de esta forma la ventana de oportunidad y por tanto la ganancia de la idea.

¿Cómo podemos los desarrolladores minimizar este periodo al máximo? La respuesta es sencilla… estando seguros en todo momento que lo que se va a subir a producción es funcional y técnicamente correcto desde la fase de desarrollo; y esto solo se puede conseguir con tests y procesos automáticos que podamos repetir una y otra vez. De esta forma podríamos hacer subidas a producción con total confianza varias veces al día si fuera necesario.

En este tutorial voy a aportar mi granito de arena para todos los que desarrollan con el framework JavaScript de moda, por su sencillez, flexibilidad y productividad: Angular.


3. Preparación del entorno

Uno de los puntos clave a la hora de implementar tests con Angular es la configuración adecuada del entorno con Jasmine, Karma y Protractor; que gracias al maravilloso @angular/cli ya tenemos de serie; así que simplemente tenemos que tener una instancia de NodeJS con npm y ejecutar:

Y creamos un proyecto, en el taller creamos el siguiente:

Ahora abrimos el proyecto con un editor de textos, a mí el que más me gusta es Visual Studio Code porque tiene una integración perfecta con TypeScript y gracias a estos plugins favorece nuestra productividad: (sé de más de uno que en el taller empezó con otro y se pasó rápido a VSCode)

  • Auto Import: nos quita de lo más tedioso de trabajar con TypeScript que es hacer los imports necesarios.
  • Angular Language Service: nos permite el autocompletado en los ficheros en los templates de los componentes y marca error cuando interpolamos una variable que no está definida como atributo.
  • TSLint: en el propio editor nos marca los errores de estilo y como se integra con codelyzer nos aplica las reglas de estilo definidas por el equipo de Angular.
  • Sort TypeScript Imports: nos da un atajo de teclado (o al salvar el fichero) para organizar los imports que utilicemos.
  • TypeScript Hero: nos facilita un atajo de teclado para eliminar todos los imports que no se estén utilizando en el fichero.
  • vscode-icons: muestra un icono distinto en función de la naturaleza del fichero, ayuda a identificar más rápidamente los ficheros.
  • bma-coverage: esta es una extensión especifica de testing que se integra con el fichero lcov generado al ejecutar los tests con el flag –code-coverage para marcar en el código si esa línea tiene cobertura o no.

Después de importar estas extensiones algunas de ellas llevan una configuración adicional dentro del fichero Preferences –> Settings que tiene que quedar de esta forma:

A destacar la propiedad “tslint.autoFixOnSave” que aplica todas las normas del fichero tslint automáticamente al guardar el fichero.

Probamos los tests que vienen de serie cuando creamos el proyecto con angular-cli con el comando:

Nota que no he usado directamente el comando ng de angular-cli sino con npm ejecutando la tarea que viene definida en la sección “scripts” del package.json; de esta forma nos aseguramos de estar usando la versión de angular-cli local y no la que tengamos instalada de forma global para generarlos. Además incluyo la opción –code-coverage con lo que verás que genera la carpeta coverage con los informes en HTML y el fichero lcov.info.

Como ves todos los tests están en verde así que tenemos un buen punto de partida.


4. Vamos al lío

Vamos a desarrollar una aplicación que recupere los primeros usuarios de GitHub atacando al API (https://api.github.com/users) y por cada uno muestre por pantalla los campos: login, avatar, url y admin. Para ello lo primero que vamos a hacer es crear el componente que se encargará de mostrarlos por pantalla, gracias al angular-cli esto es tan sencillo como ejecutar:

Esto nos va a crear una carpeta con los ficheros del componente, entre ellos un .spec que como la mayoría no sabe lo que es, tiende a borrarse de forma inmediata, pero para eso está este tutorial. 😉

Ahora pensamos en la solución y, por favor, que a nadie se le ocurra utilizar el servicio Http directamente en el componente. Yo, este tipo de componentes los estructuro en tres capas: un servicio de proxy que solo tiene como misión conectar con el API y devolver la respuesta, un servicio de adaptación de la respuesta que viene del servidor al modelo de mi aplicación y el componente que se encarga de visualizar esta información por pantalla.

Por tanto creamos el servicio de proxy que inicialmente, como no va a ser utilizado por nadie más, lo vamos a incluir dentro de la carpeta list-users. Para ello ejecutamos:

Y lo implementamos de esta forma:

Fíjate que el método devuelve un Observable con la Response de Angular y no hace ni debe hacer más lógica que esta. Ahora lo único que queremos verificar es que la llamada física se está haciendo correctamente, por lo tanto, tenemos que implementar un test de integración que lo verifique y no tiene sentido que hagamos un test unitario de esta parte. Así que en el fichero .spec asociado a este servicio de proxy vamos a realizar y verificar la llamada de esta forma:

Te habrás dado cuenta de que el 80% de este código ya nos lo había proporcionado Angular y que nuestra labor como desarrolladores “solo” se limita a configurar la clase TestBed con todas las dependencias necesarias, en este caso, la importación de HttpModule porque estamos usando el servicio Http y subscribirnos a la llamada para verificar el resultado. En este tipo de tests no hay que ser muy específico en los expects ya que los datos de la llamada pueden variar con frecuencia.

Podemos ejecutar el comando de test para verificar que efectivamente el test pasa; pero cuidado porque si olvidas el “async” que envuelve la función puedes estar incurriendo en un falso positivo dado que la parte asíncrona del test no se estará ejecutando. Para evitar esto te aconsejo que pongas un console.log inicialmente y veas que realmente se muestra en la consola.

Listo nuestro primer test de integración y no ha dolido mucho a que no. 😉

Tienes que tener en cuenta que una de las cosas que más complica este tipo de tests es la asincronía así que el truco está en eliminar esta asincronía en el resto de tests para lo cual vamos a crear un fake del servicio de proxy. Para crear el fake tenemos que tener en cuenta que cumpla con la misma signatura de la función que estamos utilizando, esto es, que devuelva una Observable de tipo Response, pero lo que no hacemos es inyectar el servicio Http sino que creamos un Observable síncrono con los datos de la respuesta real, la cual establecemos como constante en un fichero llamado “list-users.fake.spec.ts” (dejamos la extensión .spec para que no se incluya en el código de producción)

Y ahora lo usamos en el fake “list-users-proxy.service.fake.spec.ts” con el siguiente contenido:

De este modo cuando invoquemos al método usando esta implementación no se llamará al servicio real pero la respuesta será la misma a todos los efectos.

Es el momento de crear nuestro modelo que como he comentado anteriormente, tiene 4 campos. En este caso nos podemos plantear si construir el modelo con una interfaz o con una clase. La diferencia reside en que con la interfaz no añades más código a la aplicación, es simplemente para que el IDE te pueda autocompletar este tipo de datos; mientras que con la clase sí estás añadiendo código real y no es tan flexible a la hora de inicializar los datos a través del constructor.

A mí últimamente me gusta más hacerlo con interfaces así que la podemos crear con el siguiente comando:

Con el siguiente contenido:

El siguiente paso es crear el servicio que adapta los datos de la respuesta al modelo. Para ello ejecutamos:

Este servicio va a inyectar el proxy y gracias a la programación reactiva que ofrece la librería rxjs podemos fácilmente hacer el mapeo entre los campos de la respuesta y el modelo de nuestro negocio que, como es nuestro caso, no tienen por qué coincidir y nos permite desacoplarnos de la respuesta y dar un sentido semántico al desarrollo que facilita la legibilidad y el mantenimiento de la aplicación. El contenido de este servicio es el siguiente:

Ahora vamos a configurar e implementar el test asociado que como vamos a utilizar el fake será un test unitario no haciendo falta la implementación de un test de integración. Este es el contenido del test:

Fíjate que la clave está en la definición del provider donde establecemos que la implementación la proporcione el fake creado, de esta forma no necesitamos la función async y podemos ser más específicos en los expects dado que esta respuesta sí la estamos controlando, y solo queremos verificar que el mapeo de campos se está haciendo de forma adecuada. Ejecutamos el comando de test y vemos que todos los tests van pasando y que tenemos un buen grado de cobertura.

Teniendo ya los servicios implementados y probados, es el momento de implementar nuestro componente. El cual dentro del método ngOnInit va a establecer el valor del atributo “users” que será un array de tipo User. No olvidéis establecer la suscripción para poder desubscribir y así evitar posibles “memory leaks”.

Y en el template podemos establecer el siguiente contenido atendiendo a poner los ids adecuados que faciliten los tests de aceptación. Un posible contenido (sin mucho estilo, aquí es donde digo que entrarían los diseñadores con sus componentes de Polymer supercurrados donde el desarrollador solo tiene que pasarle una estructura de datos definida para que los datos se pinten de forma corporativa y mucho más bonita) podría ser este:

Ahora implementamos el test asociado donde es muy importante que lo limitemos a verificar que los atributos del componente se establecen adecuadamente y no empecemos a liarnos a verificar elementos del DOM que van a hacer que nuestro tests sea mucho más frágil. El contenido del test unitario sería este:

La propia implementación por defecto ya nos ofrece las instancias de fixture (para comprobar el DOM) y component que no es más que la instancia del componente que nos permite llamar a los métodos y verificar los atributos.

¡Pues ya está! Nuestra aplicación implementada y probada. Ahora cualquier cambio no nos dará pánico porque habrá un test que nos dirá si estamos rompiendo funcionalidad y estaremos mucho más confiados a la hora de poner nuestros desarrollos en producción. Recuerda más vale 10 subidas pequeñas al día controlados que una cada 3 meses con un montón de funcionalidad que no da tiempo a verificar en el momento de pasar a producción.

Ahora podemos configurar apropiadamente nuestro fichero “app.module.ts” con el siguiente contenido:

Por lo que ya podemos arrancar nuestra aplicación con el comando:

Y verificar que la funcionalidad es correcta. Este es un punto fundamental en los tests de aceptación que necesitan que la aplicación esté desarrollada y corriendo.

Angular almacena los tests de aceptación en la carpeta e2e y maneja el patrón Page Object donde tenemos un fichero .po que almacena las funciones de acceso al DOM y otros .spec que implementan los tests haciendo uso de los .po.

De este modo lo primero es crear el fichero list-users.po.ts dentro de la carpeta e2e con el siguiente contenido, donde a través de los elementos de protractor nos quedamos con la instancia del DOM del primer usuario a través del id que le hemos puesto.

Ahora creamos el fichero “list-users.spec.ts” donde hacemos el flujo de cargar la aplicación y verificar que el primer usuario es ‘mojombo’

Ahora podemos ejecutar estos tests con el comando:

Obviamente este tipo de tests son los más frágiles pero sí que nos valen para registrar los flujos más críticos de nuestra aplicación y lanzarlos como “smoke tests” en cualquier entorno para verificar que una subida a producción se ha hecho de forma satisfactoria, por ejemplo. Esto es mucho más rápido y efectivo que 10 equipos quedando a las 4.00 am para subir a producción y de 4.05 am a varias horas después están verificando manualmente que no han roto nada (esto lo he vivido en cliente); cuando con este tipo de tests en cuestión de minutos y de forma automática está verificado y si fallan se puede configurar para hacer un rollback a la versión anterior.

Nota: es posible que tengáis que adaptar los ficheros de tests que vienen con la configuración inicial, simplemente borrad todos aquellos casos de tests que ya no tengan validez.

5. Conclusiones

Como ves no es tan complicado hacer las cosas bien y vivir mucho más tranquilos dejando el temor de pasar a producción y ayudando a que el mantenimiento sea mucho menos costoso. Pudiendo hacer realidad las fantasías de cualquier persona de negocio de ver su idea en producción, realmente, lo antes posible.

Si te quedan dudas de cómo hacer esto o quieres que te ayudemos, contáctanos y hablamos.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.