Aprende a implementar test unitarios en Angular para asegurar la calidad de tu código. Descubre buenas prácticas, ejemplos prácticos y mejora la robustez de tus aplicaciones Angular.
Los test unitarios son una parte esencial del desarrollo de software, permitiendo asegurar que cada parte de tu aplicación funcione correctamente de manera aislada. En Angular, realizar test unitarios es crucial para mantener la calidad y robustez del código. Angular utiliza herramientas como Jasmine para escribir los tests y Karma para ejecutarlos, lo que facilita el proceso de prueba de componentes, servicios y otras partes de la aplicación.
Antes de comenzar a escribir tests unitarios, es importante tener una configuración adecuada. Angular CLI simplifica este proceso, ya que al crear un nuevo proyecto, genera una configuración básica de testing utilizando Jasmine y Karma.
ng new my-app
cd my-app
ng test
Esta configuración inicial incluye archivos de configuración como karma.conf.js y test.ts, y también instala las dependencias necesarias para realizar pruebas.
Los tests unitarios en Angular generalmente se escriben usando Jasmine, un framework de testing popular. Aquí tienes un ejemplo básico de cómo escribir un test para un componente Angular:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AppComponent ]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it('should have as title "my-app"', () => {
expect(component.title).toEqual('my-app');
});
});
Este test verifica si el componente se crea correctamente y si la propiedad title tiene el valor esperado.
Para entender mejor cómo funcionan los tests unitarios, es útil conocer los conceptos de mocks y stubs. Estos son términos que se utilizan para simular partes del código durante las pruebas. Vamos a desglosar estos conceptos.
Un stub es como un amigo que te ayuda a hacer una tarea simple. Por ejemplo, si quieres practicar matemáticas pero no quieres hacer todos los cálculos por ti mismo, podrías pedirle a tu amigo que te dé los resultados de esos cálculos específicos. Así, solo te concentras en entender cómo usar esos resultados en lugar de hacer los cálculos tú mismo.
En el contexto del software, un stub es un pedazo de código que sustituye una parte de tu programa que necesitas para la prueba, pero que no quieres ejecutar realmente. Imagina que tienes un servicio en tu aplicación que obtiene datos de una base de datos, pero para la prueba, solo quieres asegurarte de que tu aplicación maneje esos datos correctamente, no que realmente obtenga datos de la base de datos. Aquí es donde un stub sería útil. El stub proporcionará datos fijos para tus pruebas en lugar de hacer una llamada real a la base de datos.
Un mock es como un amigo que no solo te da los resultados, sino que también te dice cómo debería comportarse durante el juego. Por ejemplo, si quieres probar si tu personaje está usando una habilidad correctamente, tu amigo puede actuar como si fuera esa habilidad, dándote un comportamiento esperado y diciendo si lo usaste correctamente.
En el desarrollo de software, un mock es una herramienta que no solo proporciona datos de prueba, sino que también verifica si se usan correctamente. Imagina que tienes un componente en tu aplicación que necesita comunicarse con un servicio para obtener información. En lugar de llamar al servicio real durante la prueba, puedes usar un mock que simule el comportamiento del servicio. Además, puedes verificar si el componente interactúa con el mock de la manera que esperabas, como asegurarte de que se haya llamado a ciertas funciones o que se haya pasado la información correcta.
Asegúrate de que tus tests sean independientes y no dependan de otros tests. Esto ayuda a identificar errores de manera más eficiente y evita falsos positivos o negativos. Utiliza beforeEach para preparar el entorno de test y afterEach para limpiar cualquier estado global si es necesario.
Utiliza mocks y stubs para simular dependencias externas y enfocar los tests en la lógica del componente o servicio que estás probando. Esto es especialmente útil cuando pruebas servicios que dependen de APIs externas o bases de datos.
Mantén un alto nivel de cobertura de código, pero no te obsesiones con el 100%. Enfócate en las partes críticas de tu aplicación y asegúrate de que estén bien probadas. Usa herramientas de cobertura como karma-coverage para generar informes y identificar áreas no cubiertas.
Los servicios son una parte fundamental de cualquier aplicación Angular. Aquí hay un ejemplo de cómo testear un servicio que realiza una llamada HTTP:
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DataService } from './data.service';
describe('DataService', () => {
let service: DataService;
let httpMock: HttpTestingController;
const mockData = ['item1', 'item2', 'item3'];
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DataService]
});
service = TestBed.inject(DataService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should fetch data', () => {
service.getData().subscribe(data => {
expect(data).toEqual(mockData);
});
const req = httpMock.expectOne('https://api.example.com/data');
expect(req.request.method).toBe('GET');
req.flush(mockData);
});
});
En este ejemplo, HttpClientTestingModule se usa para simular las llamadas HTTP y HttpTestingController permite verificar las solicitudes realizadas.
Probar componentes puede ser más complejo debido a la interacción con el DOM. Aquí un ejemplo avanzado que incluye pruebas de interacción con el usuario y comprobación del estado del componente:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [ MyComponent ]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should render title in a h1 tag', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Welcome to MyComponent!');
});
it('should update title when button is clicked', () => {
component.title = 'Initial Title';
fixture.detectChanges();
const button = fixture.debugElement.query(By.css('button')).nativeElement;
button.click();
fixture.detectChanges();
expect(component.title).toBe('Title Updated');
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Title Updated');
});
});
En este ejemplo, se prueba la actualización del título en respuesta a un evento de clic en un botón.
Las directivas en Angular a menudo manipulan el DOM. Aquí tienes un ejemplo de cómo testear una directiva que cambia el color de fondo de un elemento:
import { Directive, ElementRef, Renderer2 } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef, private renderer: Renderer2) {
this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'yellow');
}
}
describe('HighlightDirective', () => {
it('should highlight the background color', () => {
const directive = new HighlightDirective(new ElementRef(document.createElement('div')), new Renderer2());
const div = document.createElement('div');
directive.ngOnInit();
expect(div.style.backgroundColor).toBe('yellow');
});
});
Este test verifica si la directiva aplica el estilo de fondo esperado al elemento.
Los pipes en Angular transforman datos en plantillas. Aquí tienes un ejemplo de cómo testear un pipe personalizado:
import { Pipe, PipeTransform } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Pipe({ name: 'reverse' })
export class ReversePipe implements PipeTransform {
transform(value: string): string {
return value.split('').reverse().join('');
}
}
describe('ReversePipe', () => {
let pipe: ReversePipe;
beforeEach(() => {
pipe = new ReversePipe();
});
it('should reverse the input string', () => {
const result = pipe.transform('hello');
expect(result).toBe('olleh');
});
});
En este test, se verifica si el pipe invierte correctamente una cadena de texto.
Realizar tests unitarios en Angular es una práctica esencial para asegurar la calidad y robustez de tu aplicación. Al seguir las buenas prácticas y entender conceptos clave como mocks y stubs, podrás crear pruebas más efectivas y mantener tu código confiable. Recuerda que los tests no solo ayudan a encontrar errores, sino que también te permiten refactorizar y mejorar tu código con confianza.