Appearance
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')
})
});
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
}
ts
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } 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 {
private http = inject(HttpClient);
readonly url: string = 'https://reqres.in/api/login'
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';
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')
}
- Nous définissons une variable
authService
qui sera utilisée pour manipuler et accéder au serviceAuthService
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');
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();
pour vérifier si la fonction a été appelée. Ou encore :
javascript
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)
});
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 notrerouterMock
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)
})
- 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')
})
- 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')
})
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')
})