¿Qué aprenderá?

Al finalizar este tutorial el estudiante estará en capacidad de desarrollar pruebas unitarias para una aplicación Angular.

¿Qué construirá?

Un conjunto de pruebas unitarias para una aplicación básica de ejemplo. La aplicación contiene una serie de "Posts" y por cada uno se cuenta con un botón "Like" y otro "Dislike". Dentro de cada botón está el número de veces que los usuarios han seleccionado Like o Dislike. La Figura 1 muestra la aplicación en ejecución.

Figura 1. Aplicación en ejecución

¿Qué necesita?

Es necesario que conozca:

  1. La estructura de un proyecto de Angular: módulos, componentes, servicios.
  2. La funcionalidad de Angular-Cli.
  3. El concepto y propósito de las pruebas unitarias.

Antes de iniciar, por favor clone el repo con el código base en su máquina de trabajo.

  1. Abra el proyecto en VSCode
  2. En el directorio raíz del proyecto, ejecute: npm install
  3. Cuando termine, ejecute: ng serve
  4. Ejecute la aplicación localhost:4200 y verifique que obtenga una lista de tres posts.

Figura 2. Aplicación desplegada correctamente.

La aplicación que se va a probar está compuesta por un módulo principal AppModule y dos módulos PostModule y LikeModule.

Módulo PostModule

En el módulo PostModule hay un componente PostListComponent cuya responsabilidad es listar los posts.

El componente PostListComponent utiliza (inyecta) un servicio PostService. De este servicio, el componente invoca el método getPosts(), el cual utiliza http para obtener un arreglo de posts. Cada elemento es de la clase Post que tiene como atributos: id, name y content.

El método getPosts() utiliza HttpClient para conectarse a una URL y traer un conjunto de posts. Por simplicidad, la URL a la cual se conecta el servicio hace referencia a un archivo estático ubicado en el directorio src/assets del proyecto.

El siguiente es el código del archivo post.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

import { Post } from "./post";

@Injectable({
 providedIn: 'root'
})

export class PostService {
 configUrl = '../assets/data.json';

 constructor(private http: HttpClient) { }

 getPosts(): Observable<Post[]> {
   return this.http.get<Post[]>(this.configUrl);
 }

}

Módulo LikeModule

En el módulo LikeModule hay un componente LikeComponent que despliega dos botones (Like y Dislike) y un texto que representa el número de likes.

En el componente LikeComponente el botón Like incrementa en uno el número de Likes del post, mientras que el botón Dislike incrementa en uno el número de dislikes de post.

En este tutorial probaremos el método del servicio que se encarga de traer los post (getPosts), y en el componente LikeComponent probaremos los métodos getLikes, upLikes, getDislikes y upDislikes.

El detalle de las pruebas se presenta en la Tabla 1.

Elemento de la aplicación

Qué se va a probar

Qué archivo se debe modificar

Servicio PostService,

El servicio incluye el método getPosts.

Al llamar a ese método se debe traer un arreglo de Post.

Para esto se creará un arreglo con 10 posts. Los datos serán creados usando la librería faker.

Como este tipo de pruebas deben ejecutarse de forma aislada, la llamada al servicio se inyectará usando la librería HttpTestingController que simula una llamada al servicio.

Al llamar al servicio se comprueba que el tamaño del arreglo sea igual a 10.

app/post/post.service.spec.ts

Componente LikeComponent

El componente incluye cuatro métodos: getLikes, upLikes, getDislikes y upDislikes. .

En la especificación se incluyen cuatro pruebas:

  1. Por defecto el valor que retorna el método getLikes debe ser igual a cero.
  2. Por defecto el valor que retorna el método getDislikes debe ser igual a cero.
  3. Cuando se llama al método upLikes el valor que retorna el método getLikes debe ser igual a uno.
  4. Cuando se llama al método upDislikes el valor que retorna el método getDislikes debe ser igual a uno.

app/like/like.component.spec.ts

Tabla 1. Detalle de las pruebas a realizar

Cuando desde Angular-Cli se crea el servicio PostService y los componentes PostListComponent y LikeComponent, también se crean tres archivos denominados post.service.spec.ts, post-list.component.spec.ts y like.component.spec.ts en los cuales se especificarán las pruebas tanto de los servicios como de los componentes.

Este es el contenido inicial del archivo post.service.spec.ts:

import { TestBed, async, inject } from '@angular/core/testing';
import { PostService } from './post.service';

describe('Service: Post', () => {
 beforeEach(() => {
   TestBed.configureTestingModule({
     providers: [PostService]
   });
 });

 it('should ...', inject([PostService], (service: PostService) => {
   expect(service).toBeTruthy();
 }));
});

Este es el contenido inicial del archivo post.list.component.spec.ts:

/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { PostListComponent } from './post-list.component';

describe('PostListComponent', () => {
 let component: PostListComponent;
 let fixture: ComponentFixture<PostListComponent>;

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

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

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

Y este es el contenido inicial del archivo like.component.spec.ts:

/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { LikeComponent } from './like.component';

describe('LikeComponent', () => {
 let component: LikeComponent;
 let fixture: ComponentFixture<LikeComponent>;

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

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

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

Si se analiza el contenido del archivo post.service.spec.ts se tienen los siguientes elementos:

Por defecto, el archivo anterior contiene una prueba que se encarga de evaluar que el servicio exista.

En la prueba de los componentes, hay un pequeño cambio dado que se incluyen dos bloques beforeEach: uno de naturaleza síncrona y otro de naturaleza asíncrona. El asíncrono inicializa el componente y síncrono se encarga de detectar cambios en la vista del componente y actúa en consecuencia ejecutando las pruebas de los bloques it.

Vamos a ejecutar todas las pruebas que se generan por defecto gracias al Angular-Cli. Haremos un cambio en la prueba por defecto del servicio (post.service.spec.ts) para que en la ejecución la podamos identificar de forma fácil.

Modifique el primer parámetro del método it para darle un nombre descriptivo a la prueba. Por ejemplo: ‘should create service'.

import { TestBed, async, inject } from '@angular/core/testing';
import { PostService } from './post.service';

describe('Service: Post', () => {
 beforeEach(() => {
   TestBed.configureTestingModule({
     providers: [PostService]
   });
 });

 it('should create service', inject([PostService], (service: PostService) => {
   expect(service).toBeTruthy();
 }));
});

Ejecutar las pruebas

Para esto abra una nueva ventana de la terminal en VSCode y escriba: ng test .

Verificar los resultados de las pruebas

En la traza de la ejecución de la prueba podemos ver que Jazmine está indicando lo siguiente: la prueba post-list.component.spec.ts falla porque no hay una mención al selector <app-like>; en post.service.spec.ts la prueba falla por la razón denominada NullInjectorError: No provider for HttpClient! Esto significa que no existe un proveedor para el servicio HttpClient.

Solucionar el error

Los errores anteriores se ocasionan por dos motivos. El primero es que en la prueba de post-list.component.spec.ts no se está incluyendo en el atributo declarations el valor LikeComponent. En la prueba del servicio post.service.spec.ts el error se ocasiona porque el servicio PostService está declarando en el constructor un objeto http de tipo HttpClient. Por tal razón, las pruebas del servicio deben hacer referencia al módulo HttpClientTestingModule. Para solucionar los errores anteriores se deben hacer las siguientes modificaciones:

En el archivo post-list.component.spec.ts se debe incluir en el arreglo imports el módulo HttpClientModule. Además en el arreglo declarations se debe agregar el valor LikeComponent. De este modo el archivo de la prueba queda así:

/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { PostListComponent } from './post-list.component';
import { HttpClientModule } from '@angular/common/http';
import { LikeComponent } from 'src/app/like/like.component';

describe('PostListComponent', () => {
 let component: PostListComponent;
 let fixture: ComponentFixture<PostListComponent>;

 beforeEach(async(() => {
   TestBed.configureTestingModule({
     imports: [HttpClientModule],
     declarations: [PostListComponent, LikeComponent]
   })
     .compileComponents();
 }));

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

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

Al ejecutar de nuevo las pruebas vemos que de 3 specs, solo hay falla en el relacionado con el servicio.

Para ajustar el último error, se debe ajustar el archivo post.service.spec.ts de la siguiente forma:

import { TestBed, async, inject } from '@angular/core/testing';
import { PostService } from './post.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';


describe('Service: Post', () => {
 beforeEach(() => {
   TestBed.configureTestingModule({
     imports: [HttpClientTestingModule],
     providers: [PostService]
   });
 });

 it('should create service', inject([PostService], (service: PostService) => {
   expect(service).toBeTruthy();
 }));
});

Al hacer los cambios, Karma los detecta y ejecuta de nuevo las pruebas. Ahora podemos observar que los tests se ejecutan sin errores.

En los próximos pasos, agregaremos más pruebas unitarias.

Para iniciar la prueba, dentro del bloque describe vamos a crear tres variables que representarán el ambiente para la ejecución de pruebas (let injector: TestBed), el servicio (let service: PostService), y el cliente simulado (let httpMock: HttpTestingController). Este cliente se crea dado que el servicio se va a probar de forma aislada de otros elementos de la aplicación.

...
describe('Service: Post, () => {
 let injector: TestBed;
 let service: PostService;
 let httpMock: HttpTestingController; 
...

Al agregar las variables anteriores se debe incluir al comienzo del archivo las siguientes importaciones:

import { TestBed, inject } from '@angular/core/testing';
import { PostService } from './post.service';
import { HttpTestingController } from '@angular/common/http/testing';

Luego, en el bloque beforeEach, vamos a importar el módulo de pruebas de servicios HttpClientTestingModule que también hace parte de la librería @angular/common/http/testing. Posteriormente asignamos valores a las tres variables declaradas en el bloque describe.

beforeEach(() => {
   TestBed.configureTestingModule({
     imports: [HttpClientTestingModule],
     providers: [PostService]
   });

   injector = getTestBed();
   service = injector.get(PostService);
   httpMock = injector.get(HttpTestingController);
 });

Esto implica modificar el primer import para incluir la referencia a la función getTestBed().

import { TestBed, inject, getTestBed } from '@angular/core/testing';

Agregar el bloque afterEach

El siguiente paso es agregar, dentro del método describe, el bloque afterEach, el cual se ejecuta luego de cada prueba asegurando que no quedan solicitudes (http requests) pendientes por procesar.

afterEach(() => {
   httpMock.verify();
 });

Definir la prueba del método getPosts()

Ahora vamos a definir la prueba del método del servicio denominado getPosts(), la cual se detalla en este código que se ubica también dentro del método describe.:

it('getPost() should return 10 records', () => {

let mockPosts: Post[] = [];

for (let i = 1; i < 11; i++) {

let post = new Post(i, faker.lorem.sentence(), faker.lorem.sentence());

mockPosts.push(post);

}

service.getPosts().subscribe((posts) => {

expect(posts.length).toBe(10);

});

const req = httpMock.expectOne(() => true);

expect(req.request.method).toBe('GET');

req.flush(mockPosts);

});

Se inicia creando un arreglo de Post denominado mockPosts en el cual se agregarán 10 posts con valores diferentes (usando el ciclo for). Recuerde importar la referencia a la clase Post.

import { Post } from './post';

Para evitar el tener que inventar datos para cada post, se hace uso de la librería faker[1] la cual permite crear datos ficticios con diferentes formatos. En este caso se usa para definir el nombre y el contenido del post. Para instalar la librería use el comando npm install faker.

Agregue la referencia a la librería en el archivo de la prueba así:

import faker from "faker";

Luego se suscribe al método getPosts del servicio y se espera que el número de elementos que retorna el arreglo posts sea igual a 10.

Para simular el llamado al servicio se usa el método httpMock.expectOne el cual recibe como parámetro la URL a la cual se hace la petición http; se asegura que el request method sea GET; y se pasa al método req.flush el arreglo de post creados previamente.

Ejecutar la prueba

Si no ha cerrado la consola de ejecución de las pruebas, Karma detecta los cambios en los archivos *spec.ts y ejecutará de nuevo las pruebas. También puede lanzar de nuevo las pruebas ejecutando desde la terminal el comando ng test.

Este comando despliega una ventana del navegador Chrome, en la que se ven los tests (specs) que se han ejecutado y el número de fallas (si las hubiera).

Nótese que ahora aparecen 4 test en ejecución.

Es importante recordar que en el contexto de Angular, una prueba unitaria comprueba el componente de forma aislada, es decir, sin tener en cuenta su template.

Vamos al archivo denominado like.component.spec.ts para definir cuatro pruebas.

En la primera vamos a probar el método getLikes. Al ejecutar este método se espera que retorne el valor cero.

Para esto dentro de la suite (método describe) incluimos este código:

it("Likes should have a 0 value", () => {
   expect(component.getLikes()).toEqual(0);
 });

En la segunda vamos a probar el método getDislikes. Al ejecutar este método se espera que retorne el valor cero.

Para esto dentro de la suite incluimos este código:

it("Dislikes should have a 0 value", () => {
   expect(component.getDislikes()).toEqual(0);
 });

Luego, se debe probar el método upLikes. Cuando se llama a este método se espera que el valor de la variable like se incremente en uno. Este es el código de la prueba:

it("Likes should increment value", () => {
   component.upLikes();
   expect(component.getLikes()).toEqual(1);
 })

Finalmente se probará el método upDislikes. Cuando se llama a este método se espera que el valor de la variable dislikes se incremente en uno. Si el valor inicial de la variable es cero, entonces se espera que el valor, luego de haber llamado al método upDislikes sea 1.

 it("Dislikes should increment value", () => {
   component.upDislikes();
   expect(component.getDislikes()).toEqual(1);
 })

Ejecutar las pruebas unitarias del componente LikeComponent

Para ejecutar las pruebas, desde la terminal ejecutamos el comando ng test. Ahora en el reporte debemos tener 8 pruebas ejecutándose exitosamente.

En este tutorial, el servicio se está conectando con un archivo data.json que se encuentra en la carpeta assets.

En algunos casos el servicio se conecta con back end. Para aislar la prueba del servicio se debe indicar en la especificación del test que lo importante es el resultado del llamado al método y no la URL de conexión.

Por ejemplo, en el archivo post.service.spec.ts, se tiene la línea const req = httpMock.expectOne(() => true). Usualmente este método recibe la URL del back, pero en este caso la instrucción () => true significa que se acepta cualquier URL.

it('Method getPost() should return 10 records', () => {

   let mockPosts: Post[] = [];

   for (let i = 1; i < 11; i++) {
     let post = new Post(i, faker.lorem.sentence(), faker.lorem.sentence());
     mockPosts.push(post);
   }

   service.getPosts().subscribe((posts) => {
     expect(posts.length).toBe(10);
   });

   const req = httpMock.expectOne(() => true);
   expect(req.request.method).toBe('GET');
   req.flush(mockPosts);
 });

[1] https://www.npmjs.com/package/faker