Skip to content
JoralmoPro
TwitterHomepage

Mejorando la calidad de tus aplicaciones Angular con Spectator y Cypress: Un enfoque completo para pruebas unitarias y end-to-end

angular, testing, tutorial, programación10 min read

Holaaa 👋

Actualmente en el mundo del desarrollo de software y todo lo relacionado con agilidad, es bastante importante por nuestra parte como desarrolladores garantizar la calidad de nuestro código. Una parte fundamental de este proceso sin duda alguna es realizar pruebas automatizadas para detectar posibles errores y así poder solucionarlos antes de que exploten producción. En este tutorial veremos como aplicar pruebas unitarias en Angular con una herramienta que conocí hace poco llamada Spectator (que me pareció genial), y para que sea mas interesante también veremos como aplicar pruebas end-to-end con Cypress. Ya hablaremos mas adelante de angular, pero también mencionaremos otros tipos de pruebas que existen (solo teoría) y como se relacionan con las pruebas unitarias y end-to-end, para así mantener una cobertura mas completa de pruebas para nuestra aplicación.

Veamos un poco de teoría

¿Qué son las pruebas unitarias?

Las pruebas unitarias son un tipo de prueba en el desarrollo de software que se enfocan en verificar el funcionamiento correcto de unidades individuales de código, como funciones, métodos o componentes, de manera aislada. Estas pruebas se realizan para garantizar que cada unidad de código se comporte como se espera, validando su lógica interna y sus interacciones con otras unidades. En resumen, las pruebas unitarias se centran en probar pequeñas partes del código de forma independiente para asegurar su correcto funcionamiento.

¿Qué son las pruebas end-to-end?

Las pruebas end-to-end (e2e) son un tipo de prueba en el desarrollo de software que se centran en simular el flujo completo de una aplicación, desde el inicio hasta el final, como si se tratara de un usuario real interactuando con ella. Estas pruebas se realizan para verificar que todos los componentes y funcionalidades de la aplicación funcionen correctamente en conjunto, incluyendo la interacción con bases de datos, APIs, navegadores y otros sistemas externos. En resumen, las pruebas end-to-end se enfocan en evaluar el comportamiento global de una aplicación, validando su correcto funcionamiento desde el punto de vista del usuario final.

¿Qué vamos a hacer?

En este ejemplo, construiremos una aplicación para verificar contraseñas seguras. La aplicación constará de dos inputs y dos botones. En el primer input, podrás escribir tu contraseña y la aplicación verificará si cumple con los criterios de seguridad. Además, habrá un botón que generará automáticamente una contraseña segura sugerida.

En el segundo input, podrás añadir tu correo electrónico. Una vez que lo ingreses, se habilitará el segundo botón para simular el envío de la contraseña segura a tu correo electrónico. Después de unos segundos (simulando el envío del correo), se mostrará un mensaje indicando que el reporte de la contraseña ha sido enviado.

Con este ejemplo podremos poner en practica las pruebas antes mencionadas, pero antes de empezar, vamos a crear nuestra aplicación angular con el siguiente comando:

1ng new secure-password

outputCreateProject

Entramos al proyecto y lo ejecutamos y veremos el proyecto por defecto de angular.

baseProject

Generaré un nuevo componente Home, que será donde se encuentre la lógica de la aplicación, he instalado angular forms (@angular/forms) para poder usar los formularios de angular, también generaré otros componentes como inputs, un botón y un servicio que serán los que a continuación probaremos, pero en resumen el Home se vería algo como esto:

homePreview

Como mencione anteriormente el ejemplo es bastante sencillo (y tal vez sin mucho sentido jajaja) pero sin duda nos servirá para lo que queremos hacer, ver como spectator nos facilita la aplicación de pruebas unitarias a nuestros componentes y servicios (a diferencia de Karma y Testbed que son las herramientas que angular nos provee por defecto para realizar pruebas unitarias)

¿Qué es Spectator?

Spectator es una herramienta que te ayuda a eliminar todo el trabajo repetitivo y tedioso al escribir pruebas unitarias en Angular. Con Spectator, puedes escribir pruebas para componentes, directivas, servicios y más, sin necesidad de aprender las complejas APIs de TestBed, ComponentFixture y DebugElement.

Spectator te permite enfocarte en escribir pruebas más legibles, elegantes y simplificadas. Puedes crear pruebas de manera rápida y eficiente, sin tener que lidiar con la configuración y la complejidad de las API tradicionales.

Spectator simplifica la escritura de pruebas al proporcionar una interfaz intuitiva y expresiva. Con Spectator, puedes acceder fácilmente a los elementos DOM, inyectar dependencias, simular eventos y realizar expectativas de forma clara y concisa.

Esta descripción de Spectator se basa en la página oficial de Spectator. pero en realidad si es verdad las pruebas con Karma y sus APIs pueden ser un poco tediosas y complejas de configurar, pero menos charla y mas acción, en la pantalla que les mostré anteriormente hay varios componentes, les muestro la estructura del proyecto

projectStructure

Aquí podemos ver a mas detalle los componentes que mencione anteriormente, el home (que contiene todo), el input, el input password y el botón

components

Empecemos entonces por el componente Button, tal vez el mas sencillo

button.component.html
1<button color="primary" [disabled]="disabled" (click)="onClick($event)">
2 {{ label }}
3</button>
button.component.ts
1import { Component, Input } from '@angular/core';
2
3@Component({
4 selector: 'app-button',
5 templateUrl: './button.component.html',
6 styleUrls: ['./button.component.scss']
7})
8export class ButtonComponent {
9 @Input('disabled') disabled: boolean;
10 @Input('onClick') onClick: ($event: Event) => void;
11 @Input('label') label: string;
12
13 constructor() {
14 this.disabled = false;
15 this.onClick = () => {};
16 this.label = '';
17 }
18
19}

Este componente recibe como inputs el label (texto del botón), el onClick (función que se ejecutará al hacer click en el botón) y el disabled (para deshabilitar el botón) que por defecto es falso para que el botón esté habilitado.

Aquí podríamos aplicar algunas pruebas como por ejemplo:

  • Prueba de renderizado: Verificar que el botón se renderice correctamente con el texto correcto.
  • Prueba de deshabilitado: Verificar que el botón se deshabilite cuando se establece la propiedad disabled en true.
  • Prueba de habilitado: Verificar que el botón esté habilitado de forma predeterminada cuando no se proporciona un valor para la propiedad disabled.
  • Prueba de evento de clic: Verificar que la función onClick se llame correctamente cuando se hace clic en el botón.

Vamos a ello, pero en este punto ni siquiera hemos instalado spectator jajaja, así que lo instalamos:

1npm install --save-dev jest @types/jest jest-preset-angular @ngneat/spectator

Instalamos jest que es el framework de pruebas que usa spectator y otras librerías necesarias para la configuración, una vez instalado configuramos Jest, creando el archivo jest.config.js en la raíz del proyecto con el siguiente contenido:

jest.config.js
1module.exports = {
2 preset: 'jest-preset-angular',
3 setupFilesAfterEnv: ['<rootDir>/src/setup-jest.ts'],
4 testPathIgnorePatterns: ['/node_modules/', '/dist/'],
5 collectCoverage: true
6};

Creamos el archivo setup-jest.ts en la carpeta src con el siguiente contenido:

setup-jest.ts
1import 'jest-preset-angular/setup-jest';

y por ultimo actualizamos el archivo angular.json busca la sección "test" y actualiza el campo "builder" con el valor "jest". Debe verse como esto:

angular.json
1"test": {
2 "builder": "jest:run",
3 "options": {
4 // Resto de las opciones...
5 }
6}

Y eso es todo, ahora a testear, en nuestro archivo .spec.ts lo que debemos hacer es importar Spectator, y la función createComponentFactory, que nos permitirá crear un componente de forma aislada en cada bloque de pruebas, esta función recibe una serie de parámetros (como imports, providers, mocks) que nos servirán para construir nuestro componente a probar (son opcionales) por ultimo debemos importar nuestro componente y ahí si empezar a probar, lo básico (ver si el componente existe) sería algo como esto:

1import { Spectator, createComponentFactory } from '@ngneat/spectator';
2import { ButtonComponent } from './button.component';
3
4describe('ButtonComponent', () => {
5 let spectator: Spectator<ButtonComponent>;
6 const createComponent = createComponentFactory(ButtonComponent);
7
8 it('should create', () => {
9 spectator = createComponent();
10 expect(spectator.component).toBeTruthy();
11 });
12});

Probamos ahora que si esté funcionando, ejecutando la prueba y veremos el output exampleTestBeforeConfig

Pero ahora si continuemos con las demás pruebas, vamos 1 x 1

Prueba de renderizado: Verificar que el botón se renderice correctamente con el texto correcto.
button.component.spec.ts
1it('Debería renderizar el componente, con el texto "Click me!"', () => {
2 spectator = createComponent({
3 props: {
4 label: 'Click me!'
5 }
6 });
7 expect(spectator.query('button')).toHaveText('Click me!');
8});

Ejecutamos esta prueba y veremos el output

firstTestButtonComponent

Pero todo se ve muy conveniente no? solo para verificar haré un cambio en el componente, en el archivo .html del componente le colocaré por defecto el texto "Hola mundo" y ejecutamos la prueba nuevamente

button.component.html
1<button color="primary" [disabled]="disabled" (click)="onClick($event)">
2 Hola mundo
3</button>

Ejecutamos las pruebas

failedTestButtonComponent

Corregimos eso y continuamos con los demás tests :D

Prueba de deshabilitado: Verificar que el botón se deshabilite cuando se establece la propiedad disabled en true.
button.component.spec.ts
1it('Debería deshabilitar el botón cuando se le pasa la propiedad disabled en true', () => {
2 spectator = createComponent({
3 props: {
4 disabled: true
5 }
6 });
7 expect(spectator.query('button')).toBeDisabled();
8});
Prueba de habilitado: Verificar que el botón esté habilitado de forma predeterminada cuando no se proporciona un valor para la propiedad disabled.
button.component.spec.ts
1it('Debería estar habilitado por defecto sino se proporciona la propiedad disabled', () => {
2 spectator = createComponent();
3 expect(spectator.query('button')).not.toBeDisabled();
4});
Prueba de evento de clic: Verificar que la función onClick se llame correctamente cuando se hace clic en el botón.
button.component.spec.ts
1it('Debería ejecutar la función recibida por parámetro al hacer click en el botón', () => {
2 const mockFn = jest.fn();
3 spectator = createComponent({
4 props: {
5 onClick: mockFn
6 }
7 });
8
9 spectator.click('button');
10
11 expect(mockFn).toHaveBeenCalled();
12});

Como podemos ver se hace bastante sencillo usar spectator para este tipo de pruebas, la forma de instanciar el componente, de pasarle los parámetros necesarios, seleccionar elementos del DOM, simular eventos, etc. es bastante sencillo y legible, pero veamos un ejemplo un poquito mas "complejo" con el componente de Input.

input.component.html
1<div class="input-container">
2 <label for="{{ id }}">{{ label }}</label>
3 <input type="{{ type }}" id="{{ id }}" placeholder="{{ placeholder }}" class="input-field" [formControl]="control" (input)="emitValue()" />
4</div>
input.component.ts
1import { Component, EventEmitter, Input, Output } from '@angular/core';
2import { FormControl } from '@angular/forms';
3
4@Component({
5 selector: 'app-input',
6 templateUrl: './input.component.html',
7 styleUrls: ['./input.component.scss']
8})
9export class InputComponent {
10 @Input('id') id: string;
11 @Input('label') label: string;
12 @Input('type') type: string;
13 @Input('placeholder') placeholder: string;
14 @Input('control') control: FormControl;
15 @Output('fnEmit') fnEmit = new EventEmitter();
16
17 constructor() {
18 this.id = '';
19 this.label = '';
20 this.type = 'text';
21 this.placeholder = '';
22 this.control = new FormControl();
23 }
24
25 emitValue(): void {
26 this.fnEmit.emit();
27 }
28}

Este componente sirve para renderizar un campo de entrada de texto (input en html) para un formulario, recibe varios inputs como el id (id del input), el label (texto del label), el type (tipo de input), el placeholder (texto del placeholder) y el control (que es el control del formulario), también tiene un output que es una función que se ejecutará cuando se escriba algo en el input, para este componente podríamos aplicar las siguientes pruebas:

  • Verificar que el componente se renderice correctamente
  • Verificar que el componente se renderice correctamente con el texto correcto
  • Verificar que el componente se renderice correctamente con el tipo correcto
  • Verificar que el componente se renderice de tipo texto por defecto
  • Verificar que el componente se renderice correctamente con el placeholder correcto
  • Verificar que el componente se renderice correctamente con el id correcto
  • Verificar que el componente se renderice correctamente con el label correcto
  • Verificar que el componente se renderice correctamente con el control correcto
  • Verificar la emisión del evento de entrada (fnEmit)

Las pruebas se ven bastante parecidas a las del componente anterior, de hecho las dos primeras quedaría algo como esto:

input.component.spec.ts
1it('Debería crear el componente', () => {
2 spectator = createComponent();
3 expect(spectator.component).toBeTruthy();
4});
5
6it('Debería renderizar en componente con el texto correcto', () => {
7 spectator = createComponent({
8 props: {
9 label: 'Nombre'
10 }
11 });
12 expect(spectator.query('label')).toHaveText('Nombre');
13});

Al ejecutar estas pruebas pasan correctamente, pero tenemos un error en consola, y es que el componente no tiene un control asignado,

errorForMissedControl

Entonces para corregir esto, para esto lo que debemos hacer es inyectar el modulo de formularios de angular en el componente, cosa que Spectator nos ayuda a hacer de forma sencilla, importamos ReactiveFormsModule en el archivo .spec.ts del componente y lo inyectamos como import en el createComponentFactory, y listo, el error desaparece, de esta forma vemos como podemos trabajar con módulos de angular en Spectator

beforeImportModule

Pero continuemos con las demás pruebas, las pruebas faltantes serían algo como esto:

input.component.spec.ts
1it('Debería renderizar el con el type correcto', () => {
2 spectator = createComponent({
3 props: {
4 type: 'password'
5 }
6 });
7 expect(spectator.query('input')).toHaveAttribute('type', 'password');
8});
9
10it('Debería renderizar el componente type text por defecto', () => {
11 spectator = createComponent();
12 expect(spectator.query('input')).toHaveAttribute('type', 'text');
13});
14
15it('Debería renderizar el componente con el placeholder correcto', () => {
16 spectator = createComponent({
17 props: {
18 placeholder: 'Ingrese su nombre'
19 }
20 });
21 expect(spectator.query('input')).toHaveAttribute('placeholder', 'Ingrese su nombre');
22});
23
24it('Debería renderizar el componente con el id correcto', () => {
25 spectator = createComponent({
26 props: {
27 id: 'nombre'
28 }
29 });
30 expect(spectator.query('input')).toHaveAttribute('id', 'nombre');
31});
32
33it('Debería renderizar el componente con el label correcto', () => {
34 spectator = createComponent({
35 props: {
36 label: 'Nombre'
37 }
38 });
39 expect(spectator.query('label')).toHaveText('Nombre');
40});
41
42it('Debería renderizar el componente con el control correcto', () => {
43 const control = new FormControl();
44 spectator = createComponent({
45 props: {
46 control
47 }
48 });
49 expect(spectator.component.control).toEqual(control);
50 // expect(spectator.component.control).toBeTruthy();
51 // expect(spectator.component.control).toBeInstanceOf(FormControl);
52});

El archivo a mi parecer queda bastante sencillo y legible, además las formas de acceder a la información del componente tanto la parte del DOM como la parte de la lógica del componente es bastante sencilla, además la forma en que instancia el componente importando el modulo, no lo veo tan complejo a diferencia de como tocaría hacerlo con Karma, pero bueno continuemos con las pruebas, veamos el componente InputPassword, prácticamente es igual, a diferencia que tenemos la inyección de un servicio y 3 funciones mas que hacen parte de la lógica del componente, veamos el componente:

input-password.component.html
1<div class="input-and-bar">
2 <app-input id="passwordInput" label="Contraseña:" placeholder="Ingrese su contraseña" [control]="control" (fnEmit)="checkPasswordStrength()"></app-input>
3 <div id="passwordStrengthBar" [ngClass]="passwordStrength">
4 <small>{{ passwordStrength }}</small>
5 </div>
6</div>
7<app-button (click)="suggestPassword($event)" label="Sugerir Contraseña"></app-button>
input-password.component.ts
1import { Component, Input } from '@angular/core';
2import { FormControl } from '@angular/forms';
3import { PasswordService } from '../password.service';
4
5@Component({
6 selector: 'app-input-password',
7 templateUrl: './input-password.component.html',
8 styleUrls: ['./input-password.component.scss']
9})
10export class InputPasswordComponent {
11
12 passwordStrength: string = "";
13 @Input('id') id: string;
14 @Input('label') label: string;
15 @Input('type') type: string;
16 @Input('placeholder') placeholder: string;
17 @Input('control') control: FormControl;
18
19 constructor(private passwordService: PasswordService) {
20 this.id = '';
21 this.label = '';
22 this.type = 'text';
23 this.placeholder = '';
24 this.control = new FormControl();
25 }
26
27 ngAfterContentInit(): void {
28 this.checkPasswordStrength();
29 }
30
31 suggestPassword(event: Event): void {
32 event.preventDefault();
33 const passwordSuggested = this.passwordService.suggestStrongPassword();
34 this.control.patchValue(passwordSuggested);
35 this.checkPasswordStrength();
36 }
37
38 checkPasswordStrength(): void {
39 this.passwordStrength = this.passwordService.checkPasswordStrength(this.control.value);
40 }
41}

Para fines de seguir un buen orden, vamos a probar primero el servicio PasswordService, veamos el código

password.service.ts
1import { Injectable } from '@angular/core';
2
3@Injectable({
4 providedIn: 'root'
5})
6export class PasswordService {
7
8 private lowercaseLetters = 'abcdefghijklmnopqrstuvwxyz';
9 private uppercaseLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
10 private numbers = '0123456789';
11 private specialCharacters = '!@#$%^&*()';
12
13 constructor() { }
14
15 checkPasswordStrength(password: string): string {
16 let strength = 0;
17
18 if (password.length >= 8) {
19 strength += 33.3;
20 }
21 if (/[a-z]/.test(password) && /[A-Z]/.test(password) && /\d/.test(password)) {
22 strength += 33.3;
23 }
24 if (/[!@#$%^&*()]/.test(password)) {
25 strength += 33.3;
26 }
27
28 if (strength < 33.3) {
29 return "weak";
30 } else if (strength < 66.6) {
31 return "medium";
32 } else if (strength < 99) {
33 return "strong";
34 } else {
35 return "very-strong";
36 }
37 }
38
39 getRandomCharacter = (characters: string) => {
40 if (characters === '') return '';
41 const randomIndex = Math.floor(Math.random() * characters.length);
42 return characters[randomIndex];
43 };
44
45 suggestStrongPassword(): string {
46 let password = '';
47 password += this.getRandomCharacter(this.lowercaseLetters);
48 password += this.getRandomCharacter(this.uppercaseLetters);
49 password += this.getRandomCharacter(this.numbers);
50 password += this.getRandomCharacter(this.specialCharacters);
51 const remainingLength = 8 - password.length;
52 for (let i = 0; i < remainingLength; i++) {
53 const allCharacters = this.lowercaseLetters + this.uppercaseLetters + this.numbers + this.specialCharacters;
54 password += this.getRandomCharacter(allCharacters);
55 }
56 return password;
57 }
58
59 reportResult(data: Object): string {
60 return JSON.stringify(data, undefined, 2);
61 }
62}

Este servicio en la aplicación es el responsable de proporcionar funcionalidad relacionada con la contraseña, como la validación de fuerza y también generar una contraseña fuerte sugerida (la medición de la fuerza en este caso es medida lo mas básica posible), entonces procedamos a probar estos cuatro métodos de este servicio, con la prueba de este servicio solo quiero mostrar que también es posible probar servicios (no solo componentes) con Spectator, el archivo completo de pruebas quedaría algo como esto

password.service.spec.ts
1import { SpectatorService, createServiceFactory } from "@ngneat/spectator";
2import { PasswordService } from "./password.service";
3
4describe('PasswordService', () => {
5 let spectator: SpectatorService<PasswordService>;
6 const createService = createServiceFactory(PasswordService);
7
8 beforeEach(() => spectator = createService());
9
10 describe('checkPasswordStrength', () => {
11 it('Debería devolver "weak" para una contraseña débil', () => {
12 const password = '123456';
13 const strength = spectator.service.checkPasswordStrength(password);
14 expect(strength).toBe('weak');
15 });
16
17 it('Debería devolver "medium" para una contraseña de fuerza media', () => {
18 const password = '123456789';
19 const strength = spectator.service.checkPasswordStrength(password);
20 expect(strength).toBe('medium');
21 });
22
23 it('Debería devolver "strong" para una contraseña fuerte', () => {
24 const password = 'Abcdef123';
25 const strength = spectator.service.checkPasswordStrength(password);
26 expect(strength).toBe('strong');
27 });
28
29 it('Debería devolver "very-strong" para una contraseña muy fuerte', () => {
30 const password = 'Abcdef123!@#';
31 const strength = spectator.service.checkPasswordStrength(password);
32 expect(strength).toBe('very-strong');
33 });
34 });
35
36 describe('getRandomCharacter', () => {
37 it('Debería devolver un carácter aleatorio de una cadena de caracteres', () => {
38 const characters = 'abcdefghijklmnopqrstuvwxyz';
39 const character = spectator.service.getRandomCharacter(characters);
40 expect(characters.includes(character)).toBe(true);
41 });
42
43 it('Debería devolver vacío de una cadena de caracteres vacía', () => {
44 const characters = '';
45 const character = spectator.service.getRandomCharacter(characters);
46 expect(character).toBe('');
47 });
48
49 it('Debería devolver un carácter aleatorio de una cadena de caracteres con un solo carácter', () => {
50 const characters = 'a';
51 const character = spectator.service.getRandomCharacter(characters);
52 expect(character).toBe('a');
53 });
54 });
55
56 describe('suggestStrongPassword', () => {
57 it('Debería generar una contraseña segura de longitud 8', () => {
58 const password = spectator.service.suggestStrongPassword();
59 expect(password.length).toBe(8);
60 });
61
62 it('Debería generar una contraseña segura que contenga una letra minúscula', () => {
63 const password = spectator.service.suggestStrongPassword();
64 expect(password).toMatch(/[a-z]/);
65 });
66
67 it('Debería generar una contraseña segura que contenga una letra mayúscula', () => {
68 const password = spectator.service.suggestStrongPassword();
69 expect(password).toMatch(/[A-Z]/);
70 });
71
72 it('Debería generar una contraseña segura que contenga un número', () => {
73 const password = spectator.service.suggestStrongPassword();
74 expect(password).toMatch(/[0-9]/);
75 });
76
77 it('Debería generar una contraseña segura que contenga un carácter especial', () => {
78 const password = spectator.service.suggestStrongPassword();
79 expect(password).toMatch(/[!@#$%^&*()]/);
80 });
81 });
82
83 describe('reportResult', () => {
84 it('Debería devolver una representación en formato JSON del objeto proporcionado', () => {
85 const data = {
86 name: 'JoralmoPro',
87 age: 30,
88 city: 'Santa Marta'
89 };
90 const expectedResult = JSON.stringify(data, undefined, 2);
91 const result = spectator.service.reportResult(data);
92 expect(result).toEqual(expectedResult);
93 });
94
95 it('Debería devolver una representación en formato JSON de un objeto vacío', () => {
96 const data = {};
97 const expectedResult = JSON.stringify(data, undefined, 2);
98 const result = spectator.service.reportResult(data);
99 expect(result).toEqual(expectedResult);
100 });
101 });
102
103});

Como podemos ver, es bastante sencillo probar servicios con Spectator, solo debemos importar SpectatorService y createServiceFactory y se hace muy parecido a como se hace con los componentes, al ejecutar las pruebas veremos el output

testPasswordService

Y ahora si una vez testeado el servicio, procedemos a probar el componente que lo usa, el InputPasswordComponent, ya antes vimos su código y para que sirve así que sobre este componente aplicaremos pruebas como:

  • Prueba que el componente se crea correctamente.
  • Prueba que los inputs (id, label, type, placeholder, control) se establecen correctamente.
  • Prueba que la función checkPasswordStrength se llama apenas se crea el componente.
  • Prueba que la función checkPasswordStrength() se llama correctamente y actualiza la variable passwordStrength.
  • Prueba que al hacer clic en el botón "Sugerir Contraseña", se genera una contraseña segura y se asigna al campo de entrada de contraseña.
  • Prueba que se apliquen las clases CSS correctas en la barra de fortaleza según el valor de passwordStrength.

Ya vemos que en este componente se hace ya un poquito mas compleja la prueba, hay mas interacción con el DOM, con el servicio, con el componente, etc. pero veamos entonces la prueba mas sencilla, que se cree correctamente, teniendo en cuenta que este componente usa el servicio, y también usa el ButtonComponent y el InputComponent además de que tiene la función ngAfterContentInit() que se ejecuta y trata de detectar la fuerza de la contraseña, por lo tanto quedaría así:

input-password.component.spec.ts
1import { Spectator, createComponentFactory } from "@ngneat/spectator";
2import { InputPasswordComponent } from "./input-password.component";
3import { InputComponent } from "../input/input.component";
4import { ButtonComponent } from "../button/button.component";
5import { ReactiveFormsModule, FormControl } from "@angular/forms";
6
7describe('InputPasswordComponent', () => {
8 let spectator: Spectator<InputPasswordComponent>;
9 const createComponent = createComponentFactory({
10 component: InputPasswordComponent,
11 declarations: [ InputComponent, ButtonComponent ],
12 imports: [ ReactiveFormsModule ]
13 });
14
15 it('Debería crear el componente', () => {
16 const passwordControl = new FormControl('');
17 spectator = createComponent({
18 props: {
19 control: passwordControl
20 }
21 });
22 expect(spectator.component).toBeTruthy();
23 });
24});

Ya vemos que la prueba mas básica se extiende un poco en código ya que hay que hacer instancias de mas cosas, pero esto Spectator lo hace ver sencillo, de este modo nuestra prueba inicial ya funciona, veamos como queda el archivo completo con todas las pruebas

input-password.component.spec.ts
1import { Spectator, createComponentFactory, typeInElement } from "@ngneat/spectator";
2import { InputPasswordComponent } from "./input-password.component";
3import { InputComponent } from "../input/input.component";
4import { ButtonComponent } from "../button/button.component";
5import { ReactiveFormsModule, FormControl } from "@angular/forms";
6import { spyOn } from "jest-mock";
7
8describe('InputPasswordComponent', () => {
9 let spectator: Spectator<InputPasswordComponent>;
10 let passwordControl: FormControl;
11 const createComponent = createComponentFactory({
12 component: InputPasswordComponent,
13 declarations: [ InputComponent, ButtonComponent ],
14 imports: [ ReactiveFormsModule ]
15 });
16
17 beforeEach(() => {
18 passwordControl = new FormControl('');
19 spectator = createComponent({
20 props: {
21 control: passwordControl
22 }
23 });
24 });
25
26 it('Debería crear el componente', () => {
27 expect(spectator.component).toBeTruthy();
28 });
29
30 describe('inputs', () => {
31 it('Debería renderizar el componente con el id correcto', () => {
32 expect(spectator.query('input')).toHaveAttribute('id', 'passwordInput');
33 });
34
35 it('Debería renderizar el componente con el label correcto', () => {
36 spectator.setInput('label', 'Contraseña');
37 expect(spectator.query('label')).toHaveText('Contraseña');
38 });
39
40 it('Debería renderizar el componente con el type correcto', () => {
41 expect(spectator.query('input')).toHaveAttribute('type', 'text');
42 });
43
44 it('Debería renderizar el componente con el placeholder correcto', () => {
45 spectator.setInput('placeholder', 'Ingrese su contraseña');
46 expect(spectator.query('input')).toHaveAttribute('placeholder', 'Ingrese su contraseña');
47 });
48
49 it('Debería renderizar el componente con el control correcto', () => {
50 expect(spectator.component.control).toBe(passwordControl);
51 });
52 });
53
54 it('Debería llamar al método checkPasswordStrength al inicializar el componente', () => {
55 spyOn(spectator.component, 'checkPasswordStrength');
56 spectator.component.ngAfterContentInit();
57 expect(spectator.component.checkPasswordStrength).toHaveBeenCalled();
58 });
59
60 it('Debería llamar al método checkPasswordStrength y actualiza la variable passwordStrength al cambiar el valor del control', () => {
61 spyOn(spectator.component, 'checkPasswordStrength');
62
63 const inputElement: HTMLInputElement = spectator.query('#passwordInput input')!;
64
65 typeInElement('123456', inputElement);
66
67 spectator.detectChanges();
68
69 expect(spectator.component.checkPasswordStrength).toHaveBeenCalled();
70 expect(spectator.component.passwordStrength).toBe('weak');
71 });
72
73 it('Debería generar una contraseña segura y asignarla al control y al input al hacer click en el botón "Sugerir contraseña"', () => {
74 const buttonElement: HTMLButtonElement = spectator.query('button')!;
75 const inputElement: HTMLInputElement = spectator.query('#passwordInput input')!;
76
77 expect(inputElement.value.length).toBe(0);
78 expect(spectator.component.passwordStrength).toBe('weak');
79 buttonElement.click();
80 spectator.detectChanges();
81 expect(inputElement.value.length).toBe(8);
82 expect(spectator.component.passwordStrength).toBe('very-strong');
83 });
84
85 describe('passwordStrengthBar css class', () => {
86 let passwordStrengthBar: HTMLElement;
87 let inputElement: HTMLInputElement;
88 beforeEach(() => {
89 passwordStrengthBar = spectator.query('#passwordStrengthBar')!;
90 inputElement = spectator.query('#passwordInput input')!;
91 });
92
93 it('Debería renderizar el componente con la clase "weak" si la contraseña es débil', () => {
94 typeInElement('123456', inputElement);
95 spectator.detectChanges();
96 expect(passwordStrengthBar).toHaveClass('weak');
97 });
98
99 it('Debería renderizar el componente con la clase "medium" si la contraseña es de fuerza media', () => {
100 typeInElement('123456789', inputElement);
101 spectator.detectChanges();
102 expect(passwordStrengthBar).toHaveClass('medium');
103 });
104
105 it('Debería renderizar el componente con la clase "strong" si la contraseña es fuerte', () => {
106 typeInElement('Abcdef123', inputElement);
107 spectator.detectChanges();
108 expect(passwordStrengthBar).toHaveClass('strong');
109 });
110
111 it('Debería renderizar el componente con la clase "very-strong" si la contraseña es muy fuerte', () => {
112 typeInElement('Abcdef123!@#', inputElement);
113 spectator.detectChanges();
114 expect(passwordStrengthBar).toHaveClass('very-strong');
115 });
116 });
117});

Como podemos ver, el archivo de pruebas se extiende un poco mas, pero no es tan complejo, y la forma de acceder a los elementos del DOM, simular eventos, detectar cambios, es bastante sencilla y legible a mi parecer.

En este punto solo nos queda por probar el componente Home, que es el componente que contiene a los demás componentes, así que bueno, vamos a ello, veamos el código del componente

home.component.html
1<div class="container">
2 <h1>Verificación de Contraseñas Seguras</h1>
3 <form class="form-container" [formGroup]="form">
4 <div class="row">
5 <app-input-password id="passwordInput" [control]="passwordControl"></app-input-password>
6 </div>
7 <div class="row">
8 <app-input id="emailInput" label="Correo Electrónico:" placeholder="Ingrese su correo electrónico" [control]="emailControl"></app-input>
9 <app-button id="sendReport" [disabled]="form.invalid" (click)="sendReport($event)" label="Enviar reporte"></app-button>
10 </div>
11 <div class="loading-container" *ngIf="loading">
12 <div class="loading">
13 <span>Enviando reporte...</span>
14 </div>
15 </div>
16 <div class="report-result" *ngIf="showReportResult">
17 <span>Resultado del reporte:</span>
18 <pre>{{ reportResult() }}</pre>
19 </div>
20 </form>
21</div>
home.component.ts
1import { Component } from '@angular/core';
2import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
3import { PasswordService } from '../password.service';
4
5@Component({
6 selector: 'app-home',
7 templateUrl: './home.component.html',
8 styleUrls: ['./home.component.scss']
9})
10export class HomeComponent {
11
12 form: FormGroup;
13 passwordControl: FormControl;
14 emailControl: FormControl;
15 loading: boolean = false;
16 showReportResult: boolean = false;
17
18 constructor(private formBuilder: FormBuilder, private passwordService: PasswordService) {
19 this.form = this.formBuilder.group({
20 emailInput: ['', [Validators.required, Validators.email]],
21 passwordInput: ['', [Validators.required, Validators.minLength(8), Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*$/)]]
22 });
23
24 this.passwordControl = this.form.get('passwordInput') as FormControl;
25 this.emailControl = this.form.get('emailInput') as FormControl;
26 }
27
28 sendReport(event: Event): void {
29 event.preventDefault();
30 this.loading = true;
31 setTimeout(() => {
32 this.loading = false;
33 this.showReportResult = true;
34 }, 3000);
35 }
36
37 reportResult(): string {
38 return this.passwordService.reportResult(this.form.value);
39 }
40}

Este componente es el que se encarga de renderizar el formulario completo! además simula el envío de un reporte, y muestra el resultado del reporte, para este componente podríamos aplicar las siguientes pruebas:

  • Prueba que el componente se crea correctamente.
  • Prueba que el botón "Enviar reporte" se deshabilite cuando el formulario es inválido.
  • Prueba que el botón "Enviar reporte" se habilite cuando el formulario es válido.
  • Prueba que el botón "Enviar reporte" llame a la función sendReport() al hacer clic.
  • Prueba de envío de reporte: Verificar que el componente muestre el mensaje "Enviando reporte..." cuando se envía el reporte.
  • Prueba de visualización del reporte enviado: Verificar que el componente muestre el resultado del reporte cuando se envía el reporte.

Y bueno, veamos como quedaría el archivo de pruebas de este componente

home.component.spec.ts
1import { Spectator, createComponentFactory, typeInElement } from "@ngneat/spectator";
2import { HomeComponent } from "./home.component";
3import { InputComponent } from "../input/input.component";
4import { ButtonComponent } from "../button/button.component";
5import { InputPasswordComponent } from "../input-password/input-password.component";
6import { ReactiveFormsModule } from "@angular/forms";
7import { spyOn } from "jest-mock";
8import { fakeAsync, tick } from "@angular/core/testing";
9
10describe('HomeComponent', () => {
11 let spectator: Spectator<HomeComponent>;
12 let sendReportButton: HTMLButtonElement;
13 let passwordInput: HTMLInputElement;
14 let emailInput: HTMLInputElement;
15 const createComponent = createComponentFactory({
16 component: HomeComponent,
17 declarations: [ InputComponent, ButtonComponent, InputPasswordComponent ],
18 imports: [ ReactiveFormsModule ]
19 });
20
21 beforeEach(() => {
22 spectator = createComponent();
23 sendReportButton = spectator.query('#sendReport button') as HTMLButtonElement;
24 passwordInput = spectator.query('#passwordInput input') as HTMLInputElement;
25 emailInput = spectator.query('#emailInput input') as HTMLInputElement;
26 });
27
28 function typeValidData(): void {
29 typeInElement('Abcdef123!@#', passwordInput);
30 typeInElement('valid@mail.com', emailInput);
31 spectator.detectChanges();
32 }
33
34 function clickSendReportButton(): void {
35 sendReportButton.click();
36 spectator.detectChanges();
37 }
38
39 it('Debería crear el componente', () => {
40 expect(spectator.component).toBeTruthy();
41 });
42
43 it('Debería deshabilitar el botón "Enviar reporte" si el formulario es inválido', () => {
44 typeInElement('12345678', passwordInput);
45 typeInElement('invalidEmail', emailInput);
46 spectator.detectChanges();
47 expect(sendReportButton.disabled).toBe(true);
48 });
49
50 it('Debería habilitar el botón "Enviar reporte" si el formulario es válido', () => {
51 typeValidData();
52 expect(sendReportButton.disabled).toBe(false);
53 });
54
55 it('Debería llamar al método sendReport al hacer click en el botón "Enviar reporte"', () => {
56 spyOn(spectator.component, 'sendReport');
57 typeValidData();
58 clickSendReportButton();
59 expect(spectator.component.sendReport).toHaveBeenCalled();
60 });
61
62 it('Debería mostrar el mensaje "Enviando reporte..." al hacer click en el botón "Enviar reporte"', () => {
63 typeValidData();
64 clickSendReportButton();
65 expect(spectator.element).toHaveText('Enviando reporte...');
66 });
67
68 it('Debería mostrar el resultado del reporte al hacer click en el botón "Enviar reporte"', fakeAsync (() => {
69
70 typeValidData();
71 clickSendReportButton();
72
73 tick(3000);
74
75 spectator.detectChanges();
76
77 const reportResult = spectator.query('.report-result') as HTMLDivElement;
78
79 expect(reportResult).toBeTruthy();
80 expect(reportResult).toHaveText('Abcdef123!@#');
81 expect(reportResult).toHaveText('valid@mail.com');
82 }));
83});

Con este ultimo archivo ya habríamos cubierto nuestras pruebas de la aplicación 😎, pero hasta ahora hemos visto que me ha tocado correr los test archivo por archivo, deberíamos hacerlo como se debe y poder correr los test con ng test, para eso debemos hacer algunos cambios:

  • Instalamos @angular-builders/jest y lo configuramos en el archivo angular.json
    angular.json
    1// Resto de la configuración...
    2"test": {
    3 "builder": "@angular-builders/jest:run",
    4 "options": {
    5 "tsConfig": "./tsconfig.spec.json",
    6 "jestConfig": ["./jest.config.js"],
    7 "passWithNoTests": true
    8 }
    9}
    10// Resto de la configuración...
  • en el archivo tsconfig.spec.json nos aseguramos de cambiar "jasmine" por "jest", "node"
    tsconfig.spec.json
    1"compilerOptions": {
    2 // Resto de la configuración...
    3 "types": [
    4 "jest",
    5 "node"
    6 ]
    7 // Resto de la configuración...
    8}

Y por ultimo ajustamos el app.component.spec.ts para que quede así:

app.component.spec.ts
1import { Spectator, createComponentFactory } from "@ngneat/spectator";
2import { AppComponent } from "./app.component";
3import { HomeComponent } from "./home/home.component";
4import { InputComponent } from "./input/input.component";
5import { ButtonComponent } from "./button/button.component";
6import { InputPasswordComponent } from "./input-password/input-password.component";
7import { ReactiveFormsModule } from "@angular/forms";
8
9describe("AppComponent", () => {
10 let spectator: Spectator<AppComponent>;
11 const createComponent = createComponentFactory({
12 component: AppComponent,
13 declarations: [ HomeComponent, InputComponent, ButtonComponent, InputPasswordComponent ],
14 imports: [ ReactiveFormsModule ]
15 });
16
17 beforeEach(() => spectator = createComponent());
18
19 it("Debería crear el componente", () => {
20 expect(spectator.component).toBeTruthy();
21 });
22});

y ahora si al ejecutar ng test (cuyo equivalente sería npm run test) vemos que todo funciona correctamente

resultForAllTest

Pero mira no más ese coverage 🤤 (aunque la app no es que haga mucho, es satisfactorio jajaja)


Hasta aquí espero haber cubierto lo básico de Spectator, y también haber abordado diferentes formas de aplicar pruebas unitarias a componentes y servicios en Angular, y además lo mas importante que le pueda servir a alguien jajaja, pero hey espera, el titulo dice "Mejorando la calidad de tus aplicaciones Angular con Spectator y Cypress", y hasta ahora solo hemos visto Spectator, pero este tutorial ya está bastante extenso, dejaré por aquí el vídeo de la ejecución de Cypress y el código de la aplicación para que lo puedas revisar con un poco mas de detalle, y en otro tutorial tal vez abordemos Cypress mas en detalle, pero por ahora espero que te haya gustado este tutorial y que te haya servido de algo, si es así, no olvides darle una estrellita al repositorio y compartirlo con tus amigos, y si no te ha gustado, pues también puedes dejar tu comentario y decirme que puedo mejorar, o si tienes alguna duda, aparezco en todas las redes sociales como @JoralmoPro, aquí dejo el vídeo:

Hasta aquí espero haber cubierto lo básico de Spectator y haber abordado diferentes formas de aplicar pruebas unitarias a componentes y servicios en Angular. Lo más importante es que esta información te sea útil jajaja.

Sin embargo, el título de este tutorial es "Mejorando la calidad de tus aplicaciones Angular con Spectator y Cypress", y hasta ahora solo hemos visto Spectator. Este tutorial ya es bastante extenso, por lo que dejaré aquí el video de la ejecución de Cypress y el código fuente de la aplicación para que puedas revisarlos con más detalle.

Si te ha gustado este tutorial y te ha sido útil, no olvides darle una estrellita al repositorio y compartirlo con tus amigos. Si tienes algún comentario sobre cómo puedo mejorar o si tienes alguna pregunta, puedes encontrarme en todas las redes sociales como @JoralmoPro.

Aquí está el video:


Y aquí está el enlace al código de la aplicación: Repositorio

Espero que hayas disfrutado de este tutorial tanto como yo escribiéndolo y que te haya sido útil.

Nos vemos en línea