Creación de Pruebas Unitarias con Jasmine y Karma en Angular

Depuración y Testing en Aplicaciones Angular (II)

Rodrigo Bosarreyes
6 min readSep 30, 2023

Las pruebas unitarias y de integración desempeñan un papel fundamental en el desarrollo de aplicaciones web robustas y de alta calidad. En el contexto de Angular, Jasmine y Karma son dos herramientas esenciales que permiten a los desarrolladores realizar pruebas exhaustivas y automatizadas. En este artículo, exploraremos cómo crear pruebas unitarias y de integración en Angular utilizando Jasmine y Karma.

¿Por qué son importantes las pruebas?

Las pruebas son esenciales en el desarrollo de software por varias razones:

  1. Detección temprana de errores: Las pruebas permiten identificar problemas en una etapa temprana del desarrollo, lo que facilita y abarata la corrección de errores.
  2. Mantenibilidad: Las pruebas sirven como documentación viva de cómo debería funcionar el código. Cuando se realizan cambios, las pruebas ayudan a garantizar que las nuevas modificaciones no rompan el código existente.
  3. Confianza en el código: Con pruebas rigurosas, puedes tener confianza en que tu código funciona según lo previsto y que los cambios futuros no afectarán negativamente la funcionalidad existente.

Estructura de directorios

Por defecto, las pruebas unitarias de Angular se encuentran en archivos con extensión .spec.ts en la misma ubicación que el archivo que están probando. Por ejemplo, si tienes un archivo mi-componente.component.ts, la prueba unitaria correspondiente se llama mi-componente.component.spec.ts.

Códificando tests: test básico

Vamos a inspeccionar el siguiente test, es bastante simple, pero es suficiente para entender cómo funciona:

mi-componente.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MiComponente } from './mi-componente.component';

En esta sección, importamos las funciones y clases necesarias de Angular para realizar pruebas unitarias. ComponentFixture y TestBed son parte de las herramientas de prueba proporcionadas por Angular. También importamos el componente MiComponente que queremos probar.

describe('MiComponente', () => {

Este bloque describe el grupo de pruebas relacionadas con el componente MiComponente. Proporcionamos una cadena de texto descriptiva ('MiComponente') como primer argumento de la función describe. Esto sirve como una etiqueta para identificar este grupo de pruebas.

let fixture: ComponentFixture<MiComponente>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MiComponente],
});

fixture = TestBed.createComponent(MiComponente);
});

El bloque beforeEach se ejecuta antes de cada prueba en este grupo. En este bloque, realizamos las siguientes acciones:

  • Usamos TestBed.configureTestingModule para configurar un módulo de prueba que declara el componente MiComponente. Esto es necesario para que Angular pueda crear una instancia del componente durante la prueba.
  • Usamos TestBed.createComponent para crear una instancia del componente MiComponente y la almacenamos en la variable fixture. Esta instancia del componente se utilizará en las pruebas posteriores.
it('debe crear el componente', () => {
const componente = fixture.componentInstance;
expect(componente).toBeTruthy();
});

El bloque it define una prueba específica. En este caso, estamos probando si el componente se crea correctamente. La descripción de la prueba se proporciona como una cadena de texto ('debe crear el componente') como primer argumento de la función it.

Dentro de la función de prueba, realizamos las siguientes acciones:

  • Obtenemos una referencia al componente MiComponente utilizando fixture.componentInstance y la almacenamos en la variable componente.
  • Usamos expect(componente).toBeTruthy(); para verificar si el componente se ha creado con éxito. toBeTruthy() es una función de Jasmine que verifica si la expresión proporcionada es "verdadera" en el sentido booleano. Si el componente existe (no es null ni undefined), la prueba pasa.

¡Recuerda que tienes más ejemplos de test en nuestro proyecto AngularDex, incluídas llamadas API!

Codificando tests: LoginComponent

Ahora vamos a codificar un test un poco más complejo, ¿recuerdas el componente creado en la clase de Creación y validación de formularios con el módulo ReactiveFormsModule? Pues vamos a codificar sus tests ¡Manos a la obra!

login.component.spec.ts

En primer lugar, necesitamos configurar TestBed para crear un entorno de prueba donde podamos instanciar nuestro componente y sus dependencias.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule, FormGroup, FormBuilder, Validators } from '@angular/forms';
import { LoginComponent } from './login.component';

describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let formBuilder: FormBuilder;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [LoginComponent],
imports: [ReactiveFormsModule],
});

fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
formBuilder = TestBed.inject(FormBuilder);

fixture.detectChanges();
});

// Aquí irán las pruebas
});

En este paso, configuramos TestBed para crear un módulo de pruebas que contiene nuestro componente LoginComponent. También importamos el módulo ReactiveFormsModule para trabajar con formularios reactivos.

Una vez configurado el TestBed, podemos comenzar a codificar los tests como tal, como siempre, vamos a comenzar validando la creación del componente:

it('debe crear el LoginComponent', () => {
expect(component).toBeTruthy();
});

En la siguiente prueba se verifica si el formulario se crea correctamente y si contiene los campos email y password. También asegura que loginForm sea una instancia de FormGroup:

it('debe crear un formulario de inicio de sesión válido', () => {
expect(component.loginForm).toBeDefined();
expect(component.loginForm instanceof FormGroup).toBeTruthy();
expect(component.loginForm.get('email')).toBeDefined();
expect(component.loginForm.get('password')).toBeDefined();
});

Esta prueba verifica si los campos email y password se marcan como inválidos cuando están vacíos:

it('debe marcar los campos email y password como inválidos cuando están vacíos', () => {
const emailControl = component.loginForm.get('email');
const passwordControl = component.loginForm.get('password');

expect(emailControl?.invalid).toBeTruthy();
expect(passwordControl?.invalid).toBeTruthy();
});

Esta prueba verifica si el campo email se marca como válido cuando se le proporciona una dirección de correo electrónico válida:

it('debe marcar el campo email como válido con una dirección de correo electrónico válida', () => {
const emailControl = component.loginForm.get('email');
emailControl?.setValue('test@example.com');
expect(emailControl?.valid).toBeTruthy();
});

Esta prueba verifica si el campo password se marca como válido cuando se le proporciona una contraseña con al menos 6 caracteres:

it('debe marcar el campo password como válido con una contraseña de al menos 6 caracteres', () => {
const passwordControl = component.loginForm.get('password');
passwordControl?.setValue('password123');
expect(passwordControl?.valid).toBeTruthy();
});

Esta prueba verifica si la función onSubmit se llama cuando el formulario se envía con datos válidos:

it('debe llamar a la función onSubmit cuando el formulario se envía con datos válidos', () => {
spyOn(component, 'onSubmit');
const emailControl = component.loginForm.get('email');
const passwordControl = component.loginForm.get('password');

emailControl?.setValue('test@example.com');
passwordControl?.setValue('password123');

const formElement: HTMLFormElement = fixture.nativeElement.querySelector('form');
formElement.dispatchEvent(new Event('submit'));
fixture.detectChanges();

expect(component.onSubmit).toHaveBeenCalled();
});

Para ejecutar las pruebas unitarias, puedes utilizar el siguiente comando:

ng test

Protip: si quieres ejecutar los tests de un archivo en específico, puedes utilizar el siguiente comando:

ng test --include='**/login.component.spec.ts' 

Con esto, has completado con éxito las pruebas unitarias para el componente LoginComponent en Angular. Estas pruebas ayudarán a garantizar que el componente funcione correctamente y que los formularios se validen adecuadamente.

Spies y Mocks en Jasmine

Los spies o espías en Jasmine son una característica clave para las pruebas unitarias. Permiten rastrear y controlar el comportamiento de funciones y métodos durante las pruebas sin afectar su implementación real. Los espías se utilizan comúnmente para:

  1. Rastrear llamadas a funciones: Puedes verificar cuántas veces se ha llamado una función, con qué argumentos y en qué contexto.
  2. Controlar el comportamiento: Puedes modificar el comportamiento de una función espía para que devuelva valores específicos o lance excepciones.
  3. Evitar llamadas reales: Puedes evitar que una función real se ejecute y, en su lugar, redirigir todas las llamadas a la función espía.

Como ves, los espías son muy útiles cuando necesitamos “mockear” un servicio en Angular, de hecho, esa es su función más extendida en este framework.

Aquí hay una breve descripción de cómo funcionan los espías en Jasmine:

  • Crear un espía: Para crear un espía en Jasmine, puedes usar la función jasmine.createSpy(). Por ejemplo:
const mySpy = jasmine.createSpy('mySpy');
  • Rastrear llamadas: Puedes verificar si el espía ha sido llamado y cuántas veces usando expectativas. Por ejemplo:
expect(mySpy).toHaveBeenCalled(); // Verificar si se llamó al espía
expect(mySpy).toHaveBeenCalledTimes(3); // Verificar si se llamó 3 veces
expect(mySpy).toHaveBeenCalledWith('argumento1', 'argumento2'); // Verificar argumentos
  • Modificar el comportamiento: Puedes configurar el espía para que devuelva un valor específico o lance una excepción. Por ejemplo:
mySpy.and.returnValue(42); // El espía ahora devuelve 42
mySpy.and.throwError('Error personalizado'); // El espía lanza una excepción
  • Evitar llamadas reales: Puedes evitar que una función real se ejecute utilizando un espía. Por ejemplo, para evitar que una función real se ejecute en un servicio, puedes hacer esto:
spyOn(myService, 'funcionReal').and.callFake(() => {
// Código personalizado en lugar de la función real
});

En resumen, los espías en Jasmine te permiten controlar, rastrear y personalizar el comportamiento de funciones y métodos durante las pruebas unitarias, lo que facilita la creación de pruebas efectivas y específicas para tu código.

--

--