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
- Crea un servei
UserService
amb un mètodegetUser
que retorna un observable amb un objecte d'usuari. - Crea un component
UserComponent
que utilitzaUserService
per obtenir l'usuari i mostrar el seu nom. - Escriu una prova unitària per
UserComponent
simulantUserService
ambspyOn
.
Exercici 2: Simular un servei amb múltiples mètodes
- Afegeix un mètode
updateUser
aUserService
que actualitza l'usuari. - Modifica
UserComponent
per utilitzarupdateUser
. - Escriu una prova unitària per
UserComponent
simulantUserService
ambcreateSpyObj
.
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
- Què és Angular?
- Configuració de l'entorn de desenvolupament
- Arquitectura d'Angular
- Primera aplicació Angular
Mòdul 2: Components d'Angular
- Comprendre els components
- Crear components
- Plantilles de components
- Estils de components
- Interacció de components
Mòdul 3: Enllaç de dades i directives
- Interpolació i enllaç de propietats
- Enllaç d'esdeveniments
- Enllaç de dades bidireccional
- Directives integrades
- Directives personalitzades
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
- Introducció al client HTTP
- Fer sol·licituds HTTP
- Gestionar respostes HTTP
- Utilitzar observables
- Gestió d'errors
Mòdul 8: Gestió d'estat
- Introducció a la gestió d'estat
- Utilitzar serveis per a la gestió d'estat
- NgRx Store
- NgRx Effects
- NgRx Entity
Mòdul 9: Proves a Angular
- Proves unitàries
- Proves de components
- Proves de serveis
- Proves de cap a cap
- Simulació de dependències
Mòdul 10: Conceptes avançats d'Angular
- Angular Universal
- Optimització del rendiment
- Internacionalització (i18n)
- Tubs personalitzats
- Animacions d'Angular