Skip to content

Tester un Observable utilisé dans un composant

Voici le code:

ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component';
import { UserService } from 'src/app/core/services/user.service';
import { of } from 'rxjs';

describe('UsersComponent', () => {
    let component: UsersComponent;
    let fixture: ComponentFixture<UsersComponent>;
    let userServiceStub: Partial<UserService>;

    beforeEach(async () => {
        userServiceStub = {
            users$: of([{ id: 1, name: 'John', email: 'john@test.com' }, { id: 2, name: 'Jane', email: 'jane@test.com' }]),
            getAll: jasmine.createSpy('getAll').and.returnValue(of())
        };

        await TestBed.configureTestingModule({
            declarations: [UsersComponent],
            providers: [{ provide: UserService, useValue: userServiceStub }]
        })
        .compileComponents();

        fixture = TestBed.createComponent(UsersComponent);
        component = fixture.componentInstance;
    });

    it('should fetch users on initialization', () => {
        component.ngOnInit();
        expect(userServiceStub.getAll).toHaveBeenCalled();
    });

    it('should have users populated from observable', () => {
        component.users$.subscribe(users => {
            expect(users.length).toBe(2);
            expect(users[0].name).toBe('John');
            expect(users[1].name).toBe('Jane');
        });
    });
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UsersComponent } from './users.component';
import { UserService } from 'src/app/core/services/user.service';
import { of } from 'rxjs';

describe('UsersComponent', () => {
    let component: UsersComponent;
    let fixture: ComponentFixture<UsersComponent>;
    let userServiceStub: Partial<UserService>;

    beforeEach(async () => {
        userServiceStub = {
            users$: of([{ id: 1, name: 'John', email: 'john@test.com' }, { id: 2, name: 'Jane', email: 'jane@test.com' }]),
            getAll: jasmine.createSpy('getAll').and.returnValue(of())
        };

        await TestBed.configureTestingModule({
            declarations: [UsersComponent],
            providers: [{ provide: UserService, useValue: userServiceStub }]
        })
        .compileComponents();

        fixture = TestBed.createComponent(UsersComponent);
        component = fixture.componentInstance;
    });

    it('should fetch users on initialization', () => {
        component.ngOnInit();
        expect(userServiceStub.getAll).toHaveBeenCalled();
    });

    it('should have users populated from observable', () => {
        component.users$.subscribe(users => {
            expect(users.length).toBe(2);
            expect(users[0].name).toBe('John');
            expect(users[1].name).toBe('Jane');
        });
    });
});
ts
import { Component, OnInit } from '@angular/core'
import { Observable } from 'rxjs';
import { UserService } from 'src/app/core/services/user.service';
import { User } from 'src/app/core/user';

@Component({
    selector: 'app-users',
    templateUrl: 'users.component.html'
})
export class UsersComponent implements OnInit {
    users$: Observable<User[]> = this.userService.users$

    constructor(private userService: UserService) { }

    ngOnInit() {
        this.userService.getAll().subscribe()
    }
}
import { Component, OnInit } from '@angular/core'
import { Observable } from 'rxjs';
import { UserService } from 'src/app/core/services/user.service';
import { User } from 'src/app/core/user';

@Component({
    selector: 'app-users',
    templateUrl: 'users.component.html'
})
export class UsersComponent implements OnInit {
    users$: Observable<User[]> = this.userService.users$

    constructor(private userService: UserService) { }

    ngOnInit() {
        this.userService.getAll().subscribe()
    }
}
ts
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { BehaviorSubject, Observable, tap } from "rxjs";
import { User } from "../user.interface";

@Injectable({
    providedIn: 'root'
})
export class UserService {
    private _users$: BehaviorSubject<User[]> = new BehaviorSubject([] as User[])
    readonly url: string = 'https://jsonplaceholder.typicode.com/users'
    readonly users$: Observable<User[]> = this._users$.asObservable()

    constructor(
        private http: HttpClient
    ) {}

    getAll(): Observable<User[]> {
        return this.http.get<User[]>(this.url)
            .pipe(
                tap((users: User[]) => {
                    this._users$.next(users)
                })
            )
    }
}
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { BehaviorSubject, Observable, tap } from "rxjs";
import { User } from "../user.interface";

@Injectable({
    providedIn: 'root'
})
export class UserService {
    private _users$: BehaviorSubject<User[]> = new BehaviorSubject([] as User[])
    readonly url: string = 'https://jsonplaceholder.typicode.com/users'
    readonly users$: Observable<User[]> = this._users$.asObservable()

    constructor(
        private http: HttpClient
    ) {}

    getAll(): Observable<User[]> {
        return this.http.get<User[]>(this.url)
            .pipe(
                tap((users: User[]) => {
                    this._users$.next(users)
                })
            )
    }
}
ts
export interface User {
    id: number;
    name: string;
    username?: string;
    email: string;
    address?: {
        street: string;
        suite: string;
        city: string;
        zipcode: string;
        geo: {
            lat: string;
            lng: string;
        }
    };
    phone?: string;
    website?: string;
    company?: {
        name: string;
        catchPhrase: string;
        bs: string;
    };
}
export interface User {
    id: number;
    name: string;
    username?: string;
    email: string;
    address?: {
        street: string;
        suite: string;
        city: string;
        zipcode: string;
        geo: {
            lat: string;
            lng: string;
        }
    };
    phone?: string;
    website?: string;
    company?: {
        name: string;
        catchPhrase: string;
        bs: string;
    };
}

Explication du test

Tester le retour de l'observable est essentiel pour s'assurer que le composant fonctionne comme prévu avec les données qu'il reçoit. Pour ce faire, nous allons ajuster le test précédent pour s'assurer que users$ est correctement alimenté par le service.

1. Le Stub de UserService:

typescript
userServiceStub = {
    users$: of([{ id: 1, name: 'John', email: 'john@test.com' }, { id: 2, name: 'Jane', email: 'jane@test.com' }]),
    getAll: jasmine.createSpy('getAll').and.returnValue(of())
};
userServiceStub = {
    users$: of([{ id: 1, name: 'John', email: 'john@test.com' }, { id: 2, name: 'Jane', email: 'jane@test.com' }]),
    getAll: jasmine.createSpy('getAll').and.returnValue(of())
};

Lorsque nous testons des composants dans Angular, nous souhaitons souvent les isoler de leurs dépendances externes pour ne tester que le composant lui-même. C'est ce qu'on appelle un test unitaire. UserService est une dépendance externe pour UsersComponent.

Le code ci-dessus crée un faux service, ou "stub", pour UserService. Plutôt que d'utiliser le vrai service, qui pourrait faire des appels HTTP ou avoir d'autres comportements que nous ne souhaitons pas inclure dans notre test, nous utilisons ce stub.

  • users$: C'est un Observable qui renvoie une liste d'utilisateurs. Dans un vrai scénario, cela pourrait être une liste récupérée d'une API. Mais pour notre test, nous utilisons of() de rxjs pour créer un Observable qui renvoie simplement une liste prédéfinie d'utilisateurs.

  • getAll: Dans le vrai UserService, cette méthode pourrait faire un appel HTTP pour récupérer une liste d'utilisateurs. Ici, nous utilisons jasmine.createSpy(). Cela crée une fonction "espion" qui nous permet de suivre si elle a été appelée et avec quels arguments. and.returnValue(of()) indique que cette fonction espion doit simplement renvoyer un Observable vide lorsqu'elle est appelée.

2. Fournir le Stub à TestBed:

typescript
providers: [{ provide: UserService, useValue: userServiceStub }]
providers: [{ provide: UserService, useValue: userServiceStub }]

TestBed est la principale API d'Angular pour tester des composants et d'autres éléments. Il nous permet de créer un environnement de test pour notre composant.

  • provide: UserService: Cela indique à Angular que nous allons fournir quelque chose pour remplacer le UserService normal dans ce contexte de test.

  • useValue: userServiceStub: C'est ici que nous disons à Angular d'utiliser notre faux service (stub) à la place du vrai UserService. Ainsi, lorsque notre composant demande une instance de UserService, il recevra notre stub à la place.

3. Tester l'Observable:

typescript
component.users$.subscribe(users => {
    expect(users.length).toBe(2);
    expect(users[0].name).toBe('John');
    expect(users[1].name).toBe('Jane');
});
component.users$.subscribe(users => {
    expect(users.length).toBe(2);
    expect(users[0].name).toBe('John');
    expect(users[1].name).toBe('Jane');
});

Le code ci-dessus teste le comportement de l'observable users$ dans notre composant.

  • component.users$.subscribe(): Cela souscrit à l'observable users$. Lorsque cet observable émet des valeurs (c'est-à-dire envoie des données), la fonction à l'intérieur de subscribe() est exécutée.