¿Qué aprenderá?

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

¿Qué construirá?

Un conjunto de pruebas unitarias y de integración para una aplicación de ejemplo. La aplicación que se va a probar está compuesta por un módulo principal AppModule y dos módulos LikeModule y PostModule.

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

En el módulo Post hay un componente PostListComponent que despliega un listado de posts. Este componente invoca un servicio PostService el cual usa http para obtener los posts.

En este tutorial el orden de las pruebas iniciará con la comprobación de los métodos del servicio PostService; luego se probarán los métodos del componente Like, para finalmente probar el template de componente PostListComponent.

¿Qué necesita?

Para poder realizar este taller Ud. debe haber tener claro:

  1. Cuál es la estructura de un proyecto de Angular: módulos, componentes, servicios
  2. Entender la funcionalidad de Angular-Cli

En la aplicación se tiene un módulo PostModule. En este módulo hay un servicio PostService, que usando HttpClient se conecta a una URL y trae un conjunto de posts (ver método getPosts). 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);
 }

}

Ahora, vamos a probar el método getPosts del servicio. Para esto, cuando desde Angular-Cli se crea el servicio, también se crea un archivo denominado post.service.spec.ts en el cual se especificará la prueba. Este es el contenido inicial de ese archivo:

/* tslint:disable:no-unused-variable */

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

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

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

Si se analiza el contenido del archivo se tienen los siguientes elementos:

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

Para iniciar la prueba, dentro del bloque describe vamos a crear dos variables que representarán el ambiente para la ejecución de pruebas y el servicio.

describe('Service: Post, () => {

let injector: TestBed;

let service: PostService;

...

Dado que el servicio se va a probar de forma aislada de otros elementos de la aplicación, vamos a simular un cliente http, el cual lo hacemos definiendo una variable de tipo HttpTestingContoller que hace parte de la librería @angular/common/http/testing.

let httpMock: HttpTestingController;

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);

});

El siguiente paso es agregar el bloque afterEach, el cual se ejecuta luego de cada prueba, y se asegura que no quedan solicitudes (http requests) pendientes por procesar.

Ahora vamos a definir la prueba del método, la cual se detalla en este código:

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('../assets/data.json');

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).

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.

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.

Para ejecutar la prueba, desde la terminal ejecutamos 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).

La prueba unitaria la haremos en el componente LikeComponent. En este componente tendremos dos atributo denominado likes y dislikes que serán los encargados de almacenar el número de likes y dislikes obtenidos respectivamente.

En el constructor del componente inicializamos el valor de los atributos a cero.

Luego, vamos a cuatro tres métodos:

El código del componente es el siguiente:

import { Component, OnInit, Input } from '@angular/core';

@Component({
 selector: 'app-like',
 templateUrl: './like.component.html',
 styleUrls: ['./like.component.css']
})
export class LikeComponent implements OnInit {

 @Input()
 id: number;

 private likes: number;
 private dislikes: number;

 constructor() {
   this.likes = 0;
   this.dislikes = 0;
 }

 getLikes() {
   return this.likes;
 }

 getDislikes() {
   return this.dislikes;
 }

 upLikes() {
   this.likes++;
 }

 upDislikes() {
   this.dislikes++;
 }

 ngOnInit() {
 }

}

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.

Cuando se genera el componente Like con AngularCLI, en la carpeta src/like se crea un archivo denominado like.component.spec.ts con el siguiente contenido:

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

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

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

 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 se tienen los siguientes elementos:

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

Definir las pruebas.

Vamos a 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 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. No obstante, si antes se ha ejecutado este método el valor por defecto podría cambiar.

Este comportamiento se evita gracias al método beforeEach(). Este método (también conocido como un Global) se ejecuta antes de cada prueba y garantiza que el componente se inicializa en cada ejecución (fixture = TestBed.createComponent(LikeComponent);)

De este modo, el valor inicial de likes será siempre cero.

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);
 })

Para ejecutar las pruebas, desde la terminal ejecutamos el comando ng test.

La prueba de integración la haremos con el componente PostComponent. Este componente tiene un atributo posts que es un arreglo de Post. En el constructor se define un servicio postService que será el encargado traer desde una url el listado de posts.

import { Component, OnInit } from '@angular/core';
import { PostService } from '../post.service';
import { Post } from '../post';

@Component({
 selector: 'app-post-list',
 templateUrl: './post-list.component.html',
 styleUrls: ['./post-list.component.css']
})
export class PostListComponent implements OnInit {

 posts: Post[];

 constructor(private postService: PostService) { }

 ngOnInit() {
   this.postService.getPosts().subscribe(posts => {
     this.posts = posts;
   })
 }
}

En este componente vamos a realizar dos pruebas de integración.

La primera es comprobar la existencia de un párrafo de primer nivel con el texto "List of posts".

La segunda es comprobar la existencia de un div con un el texto "Post 1". Para esto, y con el fin de no depender del servicio, vamos a crear un nuevo Post directamente en la prueba.

Este es el código de las pruebas:

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

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

import { Post } from '../post';
import { Observable, of } from 'rxjs';

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

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

 beforeEach(() => {
   fixture = TestBed.createComponent(PostListComponent);
   component = fixture.componentInstance;
   component.posts = [new Post("Post 1", "Content of post 1")];
   fixture.detectChanges();
   debug = fixture.debugElement;

 });

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

 it("Should have an h1 tag", () => {
   expect(debug.query(By.css("h1")).nativeElement.innerText).toBe("List of posts");
 })

 it("Should have a div tag", () => {
   const tag = debug.query(By.css("div")).nativeElement.children;
   expect(tag.length).toBe(1);
   expect(tag[0].innerText).toContain("Post 1")
 })
});

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