Cómo usar Testing en Angular con Jasmine y Karma

Antonio Pérez20-Abr, 2020

Ya hemos hablado en innumerables ocasiones de la conveniencia de utilizar tests a la hora de desarrollar nuestras apps. De hecho, lo más aconsejable es desarrollar utilizando la metodología TDD (Test driven development) o BDD (Behavior driven development).

Están surgiendo para los distintos frameworks distintas suites de testing integradas en los propios sistemas. En el caso de Angular, normalmente debería usarse Jasmine y Karma.

Por lo tanto, vamos a definir qué son estos dos amigos que nos van a acompañar en el camino.

¿Qué es Jasmine?

Jasmine es una suite de testing que sigue la metodología Behavior Driven Development. Tiene cosas muy buenas como que no requiere un DOM para hacer los tests y la sintaxis es bastante sencilla de entender.

¿Qué es Karma?

Karma es el test-runner, es decir, el módulo que permite automatizar algunas de las tareas de las suites de testing, como Jasmine. Karma, además, ha sido desarrollado directamente por el equipo de Angular, lo cual, en cierto modo, nos da alguna garantía de que va a seguir existiendo de aquí a un tiempo, por lo que parece una buena opción.

Se puede ver que con estos dos elementos ya tenemos preparado nuestro entorno para poder añadir tests a nuestra aplicación. Entrando un poco más en detalle, vamos a recordar un poco por encima los distintos tipos de tests que podemos usar. Principalmente usaremos dos, tests unitarios y tests de integración.

Tests unitarios

Los tests unitarios normalmente nos van a permitir testear elementos individuales. Podemos usarlos, y normalmente lo haremos, para testear modelos y, en multitud de ocasiones, para comprobar que los objetos se forman de forma correcta.

Tests de integración

Los tests de integración nos van a permitir testear funcionalidades complejas dentro de una aplicación, como por ejemplo el login. Podremos crear los tests necesarios para evaluar si nuestra app está realizando la función de login correctamente, así como si rechaza también de forma correcta usuarios no autorizados. Este es un buen ejemplo de un test de integración en el que intervienen varios elementos de nuestra app.

De hecho, vamos a realizar justo esto. Vamos a recrear un ejemplo de código de una app realizada con Angular para ver cómo implementamos los tests que necesitamos. Nos vamos a saltar toda la parte de creación del proyecto y vamos directamente a generar los tests. Os dejo aquí el repo en el que lo hemos desarrollado, pero a lo largo de la explicación de los tests vamos a ir recuperando bloques de este código.

Cabe destacar que con Angular, desde hace unas cuantas versiones, no necesitamos configurar nada adicional para que funcione la suite de testing. Una vez creado el proyecto, tan solo será necesario ejecutar ng test y se ejecutarán los primeros tests, mostrándonos algo así:

Código Google Chrome angular testing

Y vemos que de forma automática nos abre una ventana del navegador mostrándonos los resultados:

Localhost Karma v4

En ambas ventanas, aunque de diferentes maneras, vemos que los tests están corriendo, pero… ¿Cómo? ¿Qué ha pasado? Si no hemos hecho nada. Tranquilos, tan solo estamos comprobando que la suite de tests está corriendo.

Vamos a añadir nuestro componente de login. Tan solo vamos a ver un detalle. Al generar nuestro componente con el comando:

ng g component login

Obtenemos el siguiente resultado en el terminal:

Código master angular testing

Fijaos en algo muy interesante: al crear el componente nos ha generado directamente los archivos del componente y, además, nos ha generado un archivo login.component.spec.ts

Cuando hacemos testing, los archivos de test que se nos generan o que generamos se llaman specs, así que estos van a ser los archivos en los que los vamos a escribir. En este momento, recién creado, tiene este aspecto:

import { async, ComponentFixture, TestBed } from ‘@angular/core/testing’;

import { LoginComponent } from ‘./login.component’;

describe(‘LoginComponent’, () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it(‘should create’, () => {
expect(component).toBeTruthy();
});
});

Vamos, en primer lugar a ver la estructura de un test. Fijaos en tres palabras clave, van a ser las que más vamos a usar:

describe(‘LoginComponent’, () => {
it(‘should create’, () => {
expect(component).toBeTruthy();
});
});

En este pequeño bloque tenemos la estructura básica de lo que vamos a hacer, en primer lugar, usamos describe para poner una descripción al grupo de tests que vamos a realizar, sobre todo para organizar los tests y que sea posible hacer seguimiento.

Con el it vamos a establecer un test en particular. En este caso, el test que se realiza es que el componente se debe crear. Simplemente es un test que se genera de forma automática cuando generamos el componente.

Por último, usamos expect para definir cómo debe funcionar este test en concreto, en este caso, que es uno de los más simples, tan solo se comprueba que el componente se ha generado.

Además, fijaos que disponemos de varios Callbacks que nos permiten realizar operaciones antes y después de ejecutar los tests, sobre todo para realizar la creación y la eliminación de los registros que vamos a usar durante los tests. Estos Callbacks son los siguientes:

  • beforeAll() => Se ejecuta antes de realizar todos los tests.
  • afterAll() => Se ejecuta después de finalizar los tests de la suite.
  • beforeEach() => Se ejecuta antes de cada uno de los tests individuales.
  • afterEach() => Se ejecuta al finalizar cada test individual.

Si ahora volvemos a ejecutar ng test vemos una pequeña diferencia con respecto a la pantalla que teníamos antes:

Cógigo karma v4.3.0

Fijaos que ahora se han ejecutado 4 tests en lugar de los 3 que teníamos anteriormente y que nuestro test de creación del componente pasa, dado que el componente existe.

En realidad, aún no hemos creado ningún test propio. Hasta ahora solo hemos trabajado con lo que nos genera Angular de forma automática.

Aplicar la metodología TDD o BDD en nuestro desarrollo implica muchos cambios a nivel mental en nuestra forma de desarrollar. Sobre todo, hay un cambio de chip importantísimo que debemos asumir si lo queremos aplicar de forma correcta, y es lo siguiente: Los tests se escriben antes que el código.

Aunque esto parezca algo insignificante, a nivel de desarrollo implica profundos cambios con respecto a cómo estamos acostumbrados a desarrollar. El principal y más importante es que debemos tener totalmente claro cómo va a funcionar y a comportarse cada uno de nuestros componentes antes de iniciar la implementación. Esto conlleva una planificación del proyecto muy exhaustiva y, aunque parezca algo que lo va a alargar mucho, a posteriori nos daremos cuenta de que, aunque efectivamente alarga la fase de estudio previo del proyecto, luego hace que su implementación sea más rápida y, sobre todo, el mantenimiento posterior mucho más sencillo.

Vamos, siguiendo esta filosofía, a testear nuestro login. Para ello vamos a crear varios tests sencillos que definimos de antemano.

  1. Debemos obtener una respuesta de ko si alguno de los campos viene vacío
  2. Debemos obtener una respuesta de ok si el usuario y la contraseña son correctos y la app debe redireccionar a la página ‘dashboard’
  3. Debemos obtener una respuesta de ko si el email del usuario no existe o si el password es incorrecto

Fijaos que hemos definido tres posibles situaciones, pero digamos que hemos cubierto la mayoría de los casos posibles en nuestro login. Ahora tenemos que implementar estos tests y hacer que pasen.

Vamos a crear el código dentro de nuestro archivo

login.component.spec.ts

Crear código dentro del archivo

Y ahora, si ejecutamos los tests vemos que, obviamente, fallan. ¿Por qué? Pues porque ni el código del servicio ni el código correspondiente al login están implementados, por lo que nuestro componente de login no hace nada y, efectivamente, los tests no pasan.

Failures Karma

En este caso fijaos que nos indica que tres de nuestros tests no han pasado. El error es, concretamente, que el botón de submit no existe. Ni siquiera hemos creado el formulario.

No obstante, hay una cosa muy importante con respecto al esquema que antes explicaba. Ya tenemos totalmente definido cómo debe funcionar nuestro sistema de login y sus reglas principales. Cuando ahora implementemos el código, tenemos totalmente claro cómo debe responder, lo que no nos deja dudas a la hora de hacerlo, sin lugar a interpretación que, como todos sabemos, es un gran problema en desarrollo.

Vamos a implementar el código del componente de login (podéis ver el código completo en este repositorio). En este caso estamos realizando un ejemplo muy sencillo, por lo que he adaptado el código tanto del formulario de login, que es extremadamente simple, como el del componente, que no realiza un login real sobre un api, sino que lo simula para que veamos exactamente como responde cada uno de los tests tal y como los hemos fijado en los specs.

Y ahora, cuando pasamos los tests obtenemos la siguiente pantalla, en la que todos nuestros tests pasan sin problema:

Jasmine 3.5

Ejemplo de Testing en Angular

Este es un ejemplo muy simple de cómo usar testing con Angular. A partir de aquí, podemos aplicar esta metodología a toda nuestra aplicación, algo que nos resultará muy útil cuando realicemos ampliaciones del desarrollo y mantenimiento de la aplicación. Si seguimos estas directrices TDD o BDD seguro que nos resulta mucho más sencillo tocar posteriormente nuestra aplicación.

Antonio Pérez

Full Stack Developer. Desarrollo con Ruby on Rails, Angular, APIs y Bases de Datos.