Skip to content

Comment tester un guard sur Angular ?

Voici le test simple d'un guard qui vérifie si la personne est bien connectée ou non

ts
import { TestBed } from '@angular/core/testing';
import { AuthService } from '../services/auth.service';
import { authGuard } from './auth.guard';
import { HttpClientModule } from '@angular/common/http';
import { Router } from '@angular/router';

describe('Tester AuthService', () => {
    let authService: AuthService

    const routerMock = {
        navigateByUrl: jasmine.createSpy('navigateByUrl')
    }

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [HttpClientModule],
            providers: [
                { provide: Router, useValue: routerMock }
            ]
        });
        authService = TestBed.inject(AuthService)
    });

    it('Tester si on a token', () => {
        authService.token = 'aa'
        const ret = TestBed.runInInjectionContext(authGuard);
        expect(ret).toBe(true)
    })

    it('Tester si on n\'a pas token', () => {
        authService.token = ''
        const ret = TestBed.runInInjectionContext(authGuard);
        expect(ret).toBe(false)
        expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/login')
    })
});
import { TestBed } from '@angular/core/testing';
import { AuthService } from '../services/auth.service';
import { authGuard } from './auth.guard';
import { HttpClientModule } from '@angular/common/http';
import { Router } from '@angular/router';

describe('Tester AuthService', () => {
    let authService: AuthService

    const routerMock = {
        navigateByUrl: jasmine.createSpy('navigateByUrl')
    }

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [HttpClientModule],
            providers: [
                { provide: Router, useValue: routerMock }
            ]
        });
        authService = TestBed.inject(AuthService)
    });

    it('Tester si on a token', () => {
        authService.token = 'aa'
        const ret = TestBed.runInInjectionContext(authGuard);
        expect(ret).toBe(true)
    })

    it('Tester si on n\'a pas token', () => {
        authService.token = ''
        const ret = TestBed.runInInjectionContext(authGuard);
        expect(ret).toBe(false)
        expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/login')
    })
});
ts
import { inject } from "@angular/core"
import { AuthService } from "../services/auth.service"
import { Router } from "@angular/router"

export const authGuard = (): boolean => {
    const auth = inject(AuthService)
    const router = inject(Router)

    if (!auth.token) {
        router.navigateByUrl('/login')
        return false
    }

    return true
}
import { inject } from "@angular/core"
import { AuthService } from "../services/auth.service"
import { Router } from "@angular/router"

export const authGuard = (): boolean => {
    const auth = inject(AuthService)
    const router = inject(Router)

    if (!auth.token) {
        router.navigateByUrl('/login')
        return false
    }

    return true
}
ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs';

type LoginPayload = { email: string, password: string }
type LoginResponse = { token: string }

const KEY_STORAGE = 'angular'

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  readonly url: string = 'https://reqres.in/api/login'
  constructor(private http: HttpClient) { }

  login(payload: LoginPayload): Observable<LoginResponse> {
     return this.http.post<LoginResponse>(this.url, payload)
      .pipe(
        tap((res: LoginResponse) => {
           this.token = res.token
        })
      )
  }

  set token(val: string) {
    localStorage.setItem(KEY_STORAGE, val)
  }

  get token(): string {
    return localStorage.getItem(KEY_STORAGE) ?? ''
  }
}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs';

type LoginPayload = { email: string, password: string }
type LoginResponse = { token: string }

const KEY_STORAGE = 'angular'

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  readonly url: string = 'https://reqres.in/api/login'
  constructor(private http: HttpClient) { }

  login(payload: LoginPayload): Observable<LoginResponse> {
     return this.http.post<LoginResponse>(this.url, payload)
      .pipe(
        tap((res: LoginResponse) => {
           this.token = res.token
        })
      )
  }

  set token(val: string) {
    localStorage.setItem(KEY_STORAGE, val)
  }

  get token(): string {
    return localStorage.getItem(KEY_STORAGE) ?? ''
  }
}

Rappel d'un Guard

Dans ce code, nous testons un authGuard qui, en Angular, est généralement utilisé pour protéger certaines routes et ne les rendre accessibles que si certaines conditions sont remplies. Ici, la condition est qu'un utilisateur doit avoir un token pour accéder à la route.

1. Importations

ts
import { TestBed } from '@angular/core/testing';
import { AuthService } from '../services/auth.service';
import { authGuard } from './auth.guard';
import { HttpClientModule } from '@angular/common/http';
import { Router } from '@angular/router';
import { TestBed } from '@angular/core/testing';
import { AuthService } from '../services/auth.service';
import { authGuard } from './auth.guard';
import { HttpClientModule } from '@angular/common/http';
import { Router } from '@angular/router';
  • TestBed: C'est un module principal utilisé pour configurer et initialiser l'environnement de test dans Angular.
  • AuthService: C'est un service que vous avez défini pour gérer l'authentification (comme obtenir, définir un token, etc.).
  • authGuard: C'est le guard que nous testons.
  • HttpClientModule: Module Angular pour effectuer des requêtes HTTP.
  • Router: Il s'agit d'un service pour gérer la navigation entre les routes.

3. Configuration initiale

ts
let authService: AuthService

const routerMock = {
    navigateByUrl: jasmine.createSpy('navigateByUrl')
}
let authService: AuthService

const routerMock = {
    navigateByUrl: jasmine.createSpy('navigateByUrl')
}
  • Nous définissons une variable authService qui sera utilisée pour manipuler et accéder au service AuthService lors de nos tests.
  • Nous créons également un routerMock pour simuler le service de routage (Router) afin que nous puissions voir quand il est appelé et avec quelles valeurs.

Rappel des Spy dans Jasmine

Qu'est-ce qu'un "spy" (espion) ?

Un "spy" dans Jasmine est une fonction qui "remplace" une fonction donnée et enregistre des informations sur ses appels, comme les arguments avec lesquels elle a été appelée, combien de fois elle a été appelée, etc. Cela est utile lorsque vous voulez vérifier comment une fonction est utilisée pendant vos tests sans réellement exécuter la fonction d'origine.

jasmine.createSpy()

jasmine.createSpy() est une méthode pour créer un nouveau spy.

Lorsque vous faites :

javascript
const navigateByUrlSpy = jasmine.createSpy('navigateByUrl');
const navigateByUrlSpy = jasmine.createSpy('navigateByUrl');

Vous créez un nouveau spy pour une fonction appelée navigateByUrl. Ce spy peut ensuite être utilisé pour "remplacer" la fonction originale navigateByUrl et enregistrer des informations sur comment elle est appelée.

Par exemple, après avoir utilisé ce spy dans votre code de test, vous pourriez faire quelque chose comme :

javascript
expect(navigateByUrlSpy).toHaveBeenCalled();
expect(navigateByUrlSpy).toHaveBeenCalled();

pour vérifier si la fonction a été appelée. Ou encore :

javascript
expect(navigateByUrlSpy).toHaveBeenCalledWith('/some-route');
expect(navigateByUrlSpy).toHaveBeenCalledWith('/some-route');

pour vérifier si elle a été appelée avec un argument spécifique.

4. Configuration avant chaque test

ts
beforeEach(() => {
    TestBed.configureTestingModule({
        imports: [HttpClientModule],
        providers: [
            { provide: Router, useValue: routerMock }
        ]
    });
    authService = TestBed.inject(AuthService)
});
beforeEach(() => {
    TestBed.configureTestingModule({
        imports: [HttpClientModule],
        providers: [
            { provide: Router, useValue: routerMock }
        ]
    });
    authService = TestBed.inject(AuthService)
});
  • TestBed.configureTestingModule: Ici, nous configurons un module pour nos tests, en important le module nécessaire (HttpClientModule) et en fournissant des dépendances.
  • Notamment, au lieu d'utiliser le vrai Router, nous le remplaçons par notre routerMock afin de pouvoir simuler et tester la navigation.

5. Tests

Test 1: Vérification avec un token

ts
it('Tester si on a token', () => {
    authService.token = 'aa'
    const ret = TestBed.runInInjectionContext(authGuard);
    expect(ret).toBe(true)
})
it('Tester si on a token', () => {
    authService.token = 'aa'
    const ret = TestBed.runInInjectionContext(authGuard);
    expect(ret).toBe(true)
})
  • Nous définissons un token pour le service d'authentification.
  • Nous exécutons ensuite notre guard et nous attendons à ce qu'il renvoie true, ce qui signifie que l'accès est autorisé.

Test 2: Vérification sans token

ts
it('Tester si on n\'a pas token', () => {
    authService.token = ''
    const ret = TestBed.runInInjectionContext(authGuard);
    expect(ret).toBe(false)
    expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/login')
})
it('Tester si on n\'a pas token', () => {
    authService.token = ''
    const ret = TestBed.runInInjectionContext(authGuard);
    expect(ret).toBe(false)
    expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/login')
})
  • Nous réinitialisons le token pour qu'il soit vide.
  • Nous exécutons notre guard et nous attendons à ce qu'il renvoie false, ce qui signifie que l'accès n'est pas autorisé.
  • De plus, nous vérifions que le router a été appelé avec l'URL /login, indiquant que l'utilisateur est redirigé vers la page de connexion.

Qu'est ce que TestBed.runInInjectionContext() ?

Essayez le code suivant:

ts
it('Tester si on n\'a pas token', () => {
    authService.token = ''
    const ret = authGuard(); // [!code  warning]
    expect(ret).toBe(false)
    expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/login')
})
it('Tester si on n\'a pas token', () => {
    authService.token = ''
    const ret = authGuard(); // [!code  warning]
    expect(ret).toBe(false)
    expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/login')
})

En lançant les tests, vous allez avoir l'erreur suivante:

Error: NG0203: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with runInInjectionContext

Si AuthGuard contient une fonction comme const auth = inject(AuthService) (voir le code plus haut), cela signifie que lors de l'exécution de cette fonction, Angular tentera d'injecter AuthService en utilisant son mécanisme d'injection. Cependant, dans un environnement de test, le mécanisme d'injection d'Angular n'est pas initialisé de la même manière qu'il le serait dans une application en cours d'exécution, d'où l'erreur d'injection.

TestBed.runInInjectionContext() est une méthode qui nous permet d'exécuter une fonction donnée dans le contexte d'injection d'Angular. En gros, cela signifie que vous pouvez appeler une fonction comme si vous étiez dans un environnement où l'injection de dépendances d'Angular est pleinement opérationnelle.

Lorsque vous utilisez TestBed.runInInjectionContext(authGuard), vous exécutez essentiellement le code à l'intérieur de authGuard dans un contexte où toutes les dépendances injectables (comme AuthService dans cet exemple) sont accessibles, évitant ainsi l'erreur d'injection.

La solution est donc :

ts
it('Tester si on n\'a pas token', () => {
    authService.token = ''
    const ret = TestBed.runInInjectionContext(authGuard);
    expect(ret).toBe(false)
    expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/login')
})
it('Tester si on n\'a pas token', () => {
    authService.token = ''
    const ret = TestBed.runInInjectionContext(authGuard);
    expect(ret).toBe(false)
    expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/login')
})