Test Unitarios en Angular: Mejora la Calidad de tu Código con Buenas Prácticas

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.

Test Unitarios en Angular: Mejora la Calidad de tu Código con Buenas Prácticas

Tabla de Contenido

Introducción

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.

Configuración Inicial

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.

Escribiendo el Primer Test

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.

Conceptos Clave: Mocks y Stubs

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.

¿Qué es un Stub?

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.

¿Qué es un Mock?

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.

Buenas Prácticas

Isolación de Tests

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.

Uso de Mocks y Stubs

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.

Cobertura de Código

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.

Ejemplos Prácticos

Testeando Servicios

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.

Testeando Componentes

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.

Testeando Directivas

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.

Testeando Pipes

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.

Conclusión

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.