Sebastian Gomez
Escribiendo tests para funciones y propiedades privadas en Angular
Vamos a suponer que ya sabes qué es Angular y cómo crear un servicio. Cuando ejecutas el comando ng generate service <nombre del servicio>, se crean dos archivos: el archivo del servicio y uno con la extensión .spec. El propósito de este último es darte una estructura fácil y comprensible para escribir pruebas.
Fundamentos
Supongamos que decides crear un servicio llamado names para devolver una lista de nombres. Guardarás los nombres en una propiedad privada y crearás un método get para devolverlos a un componente u otro servicio. Sería algo así:
// Importa el decorador 'Injectable' de '@angular/core' para definir un servicio inyectable en Angular
import { Injectable } from '@angular/core';
// Aplica el decorador 'Injectable' al servicio y especifica que estará disponible en toda la aplicación ('root')
@Injectable({
providedIn: 'root',
})
// Exporta la clase 'NamesService' como un servicio inyectable de Angular
export class NamesService {
// Define una propiedad privada 'names' que es un arreglo de cadenas con dos nombres iniciales
private names: string[] = ['Juan', 'Mati'];
// Constructor vacío para la clase 'NamesService'
constructor() {}
// Método público 'getNames' que devuelve el arreglo de nombres
public getNames() {
return this.names;
}
}En nuestra prueba unitaria, podrías intentar algo como esto:
// Importa 'TestBed' de '@angular/core/testing' para crear un entorno de pruebas
import { TestBed } from '@angular/core/testing';
// Importa la clase 'NamesService' del archivo 'names.service'
import { NamesService } from './names.service';
// Describe un conjunto de pruebas para 'NamesService'
describe('NamesService', () => {
let service: NamesService;
// Antes de cada prueba, configura el entorno y resuelve el servicio con TestBed.inject
beforeEach(() => {
// No hace falta declarar 'providers': el servicio ya usa providedIn: 'root'
TestBed.configureTestingModule({});
service = TestBed.inject(NamesService);
});
// Prueba que verifica si el servicio 'NamesService' se crea correctamente
it('should be created', () => {
// Espera que el servicio sea verdadero (instanciado)
expect(service).toBeTruthy();
});
// Prueba que verifica si 'getNames' devuelve todos los nombres
it('should return all names', () => {
// Espera que el resultado de 'getNames()' sea igual al arreglo de nombres 'names'
expect(service.getNames()).toEqual(service.names);
// Espera que el primer elemento de 'getNames()' sea igual al primer elemento de 'names'
expect(service.getNames()[0]).toBe(service.names[0]);
// El primer elemento del arreglo ['Juan', 'Mati'] es 'Juan'
expect(service.getNames()[0]).toBe('Juan');
});
});Nota sobre la API moderna. En versiones antiguas de Angular era común usar el ayudante inject([NamesService], (service) => {... }) dentro del it. Desde Angular 9 en adelante el patrón idiomático es resolver el servicio con const service = TestBed.inject(NamesService);. El ayudante inject() todavía existe, pero la documentación oficial recomienda TestBed.inject.
Sin embargo, al intentar leer la propiedad names del servicio te encontrarás con un problema: es privada, y TypeScript no te dejará acceder a ella desde la prueba. El compilador marcará un error. Para evitar esto, puedes saltarte la comprobación de TypeScript y acceder al objeto mediante la sintaxis de corchetes, como service["names"]. Mira cómo quedaría:
// Importa 'TestBed' de '@angular/core/testing' para crear un entorno de pruebas
import { TestBed } from '@angular/core/testing';
// Importa la clase 'NamesService' del archivo 'names.service'
import { NamesService } from './names.service';
// Describe un conjunto de pruebas para 'NamesService'
describe('NamesService', () => {
let service: NamesService;
// Antes de cada prueba, configura el entorno y resuelve el servicio con TestBed.inject
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(NamesService);
});
// Prueba que verifica si el servicio 'NamesService' se crea correctamente
it('should be created', () => {
// Espera que el servicio sea verdadero (instanciado)
expect(service).toBeTruthy();
});
// Prueba que verifica si 'getNames' devuelve todos los nombres
it('should return all names', () => {
// Accede a la propiedad privada 'names' con la sintaxis de corchetes
// Espera que la longitud de 'getNames()' sea igual a la de 'names'
expect(service.getNames().length).toBe(service['names'].length);
// Espera que el primer elemento de 'getNames()' sea igual al primer elemento de 'names'
expect(service.getNames()[0]).toBe(service['names'][0]);
});
});Importante: límites de la técnica. El modificador private de TypeScript existe solo en tiempo de compilación, es una pista para el compilador, no una barrera real en tiempo de ejecución. Por eso service["names"] funciona: en el JavaScript generado la propiedad es perfectamente accesible. Esto es distinto de los campos privados nativos de JavaScript, los que empiezan con # (por ejemplo #names), que sí son privados en tiempo de ejecución y no se pueden alcanzar con la sintaxis de corchetes. Si tu propiedad es #names, esta técnica no aplica.
Hay discusiones sobre si es correcto probar propiedades privadas y si esta sintaxis es apropiada. La respuesta dependerá de la filosofía del equipo y de la guía de estilo que adopten. Es útil tener una "navaja suiza" de opciones para diferentes casos y proyectos.
Conclusiones
- Es posible acceder a propiedades privadas en pruebas unitarias utilizando la sintaxis de corchetes de JavaScript, porque
privateen TypeScript es solo una comprobación de tiempo de compilación. - El enfoque que elijas puede depender de la filosofía del equipo y de la guía de estilo.
- Tener distintas opciones de prueba es útil para adaptarse a diferentes casos y proyectos.
Ejercicios propuestos
- Crea un servicio en Angular que maneje una lista de objetos en lugar de una lista de nombres y adapta las pruebas unitarias para probar sus propiedades y métodos privados.
- Escribe un conjunto de pruebas que verifique que se pueden agregar y eliminar elementos de la lista de objetos del servicio, además de probar las propiedades privadas.
- Investiga y compara diferentes enfoques para probar propiedades y métodos privados en Angular y discute con tu equipo cuál es el más apropiado para sus proyectos.
Resumen en 3 puntos
- Usa la sintaxis de corchetes de JavaScript (
service["names"]) para acceder a propiedades privadas en las pruebas unitarias de Angular, recordando que esto solo funciona conprivatede TypeScript, no con campos#private. - Resuelve los servicios con
TestBed.inject(NamesService), el patrón idiomático actual, y evita declararproviderscuando el servicio ya usaprovidedIn: 'root'. - Elige el enfoque de prueba adecuado según la filosofía del equipo y la guía de estilo, y mantén varias opciones a mano para distintos casos y proyectos.
Eso es todo, espero que este post te sea de utilidad y lo puedas aplicar a algún proyecto que tengas en mente. Si tienes alguna duda, déjame un comentario aquí abajo. Y recuerda que si te gustó, también puedes compartirlo usando los enlaces a las redes sociales en la parte inferior.
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.