En aquest tema, aprendrem com simular dependències en les proves d'Angular. La simulació de dependències és una tècnica essencial per aïllar el codi que estem provant i assegurar-nos que les proves són fiables i repetibles. Això és especialment important quan les dependències poden tenir efectes secundaris, com ara fer sol·licituds HTTP o interactuar amb serveis externs.

Objectius del tema

  • Comprendre què és la simulació de dependències.
  • Aprendre a utilitzar Jasmine per simular dependències.
  • Veure exemples pràctics de simulació de serveis en proves unitàries.

Què és la simulació de dependències?

La simulació de dependències (mocking) és el procés de crear versions falses o simulades de les dependències d'un component o servei per a les proves. Aquestes versions simulades es comporten de manera controlada i previsible, permetent-nos provar el codi en aïllament.

Avantatges de la simulació de dependències

  • Aïllament: Permet provar el codi sense dependre de les implementacions reals de les seves dependències.
  • Control: Podem controlar el comportament de les dependències simulades per provar diferents escenaris.
  • Fiabilitat: Les proves són més fiables perquè no depenen de factors externs com la xarxa o serveis externs.

Utilitzar Jasmine per simular dependències

Jasmine és el framework de proves utilitzat per defecte en Angular. Proporciona diverses eines per simular dependències, com ara spyOn i createSpyObj.

spyOn

spyOn és una funció de Jasmine que permet espiar (i opcionalment simular) els mètodes d'un objecte. Això és útil per verificar que un mètode s'ha cridat o per substituir el seu comportament durant una prova.

Exemple: Simulació d'un servei

Suposem que tenim un servei DataService amb un mètode getData que fa una sol·licitud HTTP per obtenir dades. Volem provar un component que utilitza aquest servei.

// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
    return this.http.get('https://api.example.com/data');
  }
}
// my-component.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app-my-component',
  template: '<div>{{ data }}</div>'
})
export class MyComponent implements OnInit {
  data: any;

  constructor(private dataService: DataService) {}

  ngOnInit(): void {
    this.dataService.getData().subscribe(data => {
      this.data = data;
    });
  }
}

Per provar MyComponent, podem simular DataService utilitzant spyOn.

// my-component.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my-component.component';
import { DataService } from './data.service';
import { of } from 'rxjs';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let dataService: DataService;

  beforeEach(() => {
    const dataServiceMock = {
      getData: jasmine.createSpy('getData').and.returnValue(of({ key: 'value' }))
    };

    TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [{ provide: DataService, useValue: dataServiceMock }]
    }).compileComponents();

    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    dataService = TestBed.inject(DataService);
  });

  it('should fetch data on init', () => {
    fixture.detectChanges();
    expect(dataService.getData).toHaveBeenCalled();
    expect(component.data).toEqual({ key: 'value' });
  });
});

createSpyObj

createSpyObj és una altra funció de Jasmine que permet crear un objecte amb múltiples espies. Això és útil quan volem simular un servei amb diversos mètodes.

Exemple: Simulació d'un servei amb múltiples mètodes

Suposem que DataService té diversos mètodes que volem simular.

// data.service.ts
@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
    return this.http.get('https://api.example.com/data');
  }

  postData(data: any): Observable<any> {
    return this.http.post('https://api.example.com/data', data);
  }
}

Podem utilitzar createSpyObj per simular aquest servei.

// my-component.component.spec.ts
describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let dataService: jasmine.SpyObj<DataService>;

  beforeEach(() => {
    const dataServiceMock = jasmine.createSpyObj('DataService', ['getData', 'postData']);
    dataServiceMock.getData.and.returnValue(of({ key: 'value' }));
    dataServiceMock.postData.and.returnValue(of({ success: true }));

    TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [{ provide: DataService, useValue: dataServiceMock }]
    }).compileComponents();

    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    dataService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
  });

  it('should fetch data on init', () => {
    fixture.detectChanges();
    expect(dataService.getData).toHaveBeenCalled();
    expect(component.data).toEqual({ key: 'value' });
  });

  it('should post data', () => {
    component.postData({ key: 'value' });
    expect(dataService.postData).toHaveBeenCalledWith({ key: 'value' });
  });
});

Exercicis pràctics

Exercici 1: Simular un servei senzill

  1. Crea un servei UserService amb un mètode getUser que retorna un observable amb un objecte d'usuari.
  2. Crea un component UserComponent que utilitza UserService per obtenir l'usuari i mostrar el seu nom.
  3. Escriu una prova unitària per UserComponent simulant UserService amb spyOn.

Exercici 2: Simular un servei amb múltiples mètodes

  1. Afegeix un mètode updateUser a UserService que actualitza l'usuari.
  2. Modifica UserComponent per utilitzar updateUser.
  3. Escriu una prova unitària per UserComponent simulant UserService amb createSpyObj.

Solucions

Solució a l'exercici 1

// user.service.ts
@Injectable({
  providedIn: 'root'
})
export class UserService {
  getUser(): Observable<any> {
    return of({ name: 'John Doe' });
  }
}

// user.component.ts
@Component({
  selector: 'app-user',
  template: '<div>{{ user?.name }}</div>'
})
export class UserComponent implements OnInit {
  user: any;

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.userService.getUser().subscribe(user => {
      this.user = user;
    });
  }
}

// user.component.spec.ts
describe('UserComponent', () => {
  let component: UserComponent;
  let fixture: ComponentFixture<UserComponent>;
  let userService: UserService;

  beforeEach(() => {
    const userServiceMock = {
      getUser: jasmine.createSpy('getUser').and.returnValue(of({ name: 'John Doe' }))
    };

    TestBed.configureTestingModule({
      declarations: [UserComponent],
      providers: [{ provide: UserService, useValue: userServiceMock }]
    }).compileComponents();

    fixture = TestBed.createComponent(UserComponent);
    component = fixture.componentInstance;
    userService = TestBed.inject(UserService);
  });

  it('should fetch user on init', () => {
    fixture.detectChanges();
    expect(userService.getUser).toHaveBeenCalled();
    expect(component.user).toEqual({ name: 'John Doe' });
  });
});

Solució a l'exercici 2

// user.service.ts
@Injectable({
  providedIn: 'root'
})
export class UserService {
  getUser(): Observable<any> {
    return of({ name: 'John Doe' });
  }

  updateUser(user: any): Observable<any> {
    return of({ success: true });
  }
}

// user.component.ts
@Component({
  selector: 'app-user',
  template: '<div>{{ user?.name }}</div>'
})
export class UserComponent implements OnInit {
  user: any;

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.userService.getUser().subscribe(user => {
      this.user = user;
    });
  }

  updateUser(user: any): void {
    this.userService.updateUser(user).subscribe(response => {
      if (response.success) {
        this.user = user;
      }
    });
  }
}

// user.component.spec.ts
describe('UserComponent', () => {
  let component: UserComponent;
  let fixture: ComponentFixture<UserComponent>;
  let userService: jasmine.SpyObj<UserService>;

  beforeEach(() => {
    const userServiceMock = jasmine.createSpyObj('UserService', ['getUser', 'updateUser']);
    userServiceMock.getUser.and.returnValue(of({ name: 'John Doe' }));
    userServiceMock.updateUser.and.returnValue(of({ success: true }));

    TestBed.configureTestingModule({
      declarations: [UserComponent],
      providers: [{ provide: UserService, useValue: userServiceMock }]
    }).compileComponents();

    fixture = TestBed.createComponent(UserComponent);
    component = fixture.componentInstance;
    userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
  });

  it('should fetch user on init', () => {
    fixture.detectChanges();
    expect(userService.getUser).toHaveBeenCalled();
    expect(component.user).toEqual({ name: 'John Doe' });
  });

  it('should update user', () => {
    const newUser = { name: 'Jane Doe' };
    component.updateUser(newUser);
    expect(userService.updateUser).toHaveBeenCalledWith(newUser);
    expect(component.user).toEqual(newUser);
  });
});

Conclusió

La simulació de dependències és una tècnica poderosa per aïllar el codi que estem provant i assegurar-nos que les proves són fiables i repetibles. Utilitzant spyOn i createSpyObj de Jasmine, podem simular fàcilment serveis i altres dependències en les nostres proves unitàries. Amb la pràctica, aquesta tècnica esdevindrà una part essencial del vostre arsenal de proves.

Curs d'Angular

Mòdul 1: Introducció a Angular

Mòdul 2: Components d'Angular

Mòdul 3: Enllaç de dades i directives

Mòdul 4: Serveis i injecció de dependències

Mòdul 5: Enrutament i navegació

Mòdul 6: Formularis a Angular

Mòdul 7: Client HTTP i observables

Mòdul 8: Gestió d'estat

Mòdul 9: Proves a Angular

Mòdul 10: Conceptes avançats d'Angular

Mòdul 11: Desplegament i millors pràctiques

© Copyright 2024. Tots els drets reservats