Appearance
RouterTestingHarness : Testez vos routes Angular sans effort
WARNING
Disponible depuis Angular 15.2
Imaginez que vous devez tester le système de navigation d'un site web complexe. Traditionnellement, c'était comme essayer de tester une voiture en simulant chaque pièce du moteur séparément - fastidieux et peu fiable. RouterTestingHarness
change la donne : c'est comme avoir un simulateur de conduite qui utilise un vrai moteur !
RouterTestingHarness
est une classe utilitaire révolutionnaire introduite dans Angular qui permet de tester la navigation de votre application avec le vrai Router, sans avoir besoin de créer des mocks complexes. Fini les configurations laborieuses, place à des tests simples et fiables !
Pourquoi RouterTestingHarness révolutionne les tests ?
Avant RouterTestingHarness
, tester la navigation était un véritable parcours du combattant. Voyons pourquoi cette nouvelle approche change tout :
L'ancien monde : RouterTestingModule
typescript
// Ancien code avec RouterTestingModule - complexe et verbeux
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TestHostComponent, UserComponent],
imports: [RouterTestingModule.withRoutes([
{ path: 'users/:id', component: UserComponent }
])],
providers: [UserService]
}).compileComponents();
fixture = TestBed.createComponent(TestHostComponent);
router = TestBed.inject(Router);
location = TestBed.inject(Location);
fixture.detectChanges();
});
it('should navigate to user detail', fakeAsync(() => {
router.navigate(['/users/123']);
tick();
fixture.detectChanges();
expect(location.path()).toBe('/users/123');
// Plus de code pour vérifier le rendu...
}));
Le nouveau monde : RouterTestingHarness
typescript
// Nouveau code avec RouterTestingHarness - simple et élégant
it('should display user details', async () => {
const harness = await RouterTestingHarness.create();
const component = await harness.navigateByUrl('/users/123', UserComponent);
expect(component.userId()).toBe('123');
expect(harness.routeNativeElement?.textContent).toContain('User Profile');
});
Avantages majeurs
- 90% moins de code de configuration
- Vrai Router utilisé (pas de simulation)
- Tests plus fiables car identiques au comportement en production
- API intuitive avec des méthodes explicites
Configuration de base
Pour commencer à utiliser RouterTestingHarness
, nous devons d'abord configurer notre environnement de test. Voici la structure minimale :
typescript
import { RouterTestingHarness } from '@angular/router/testing';
import { provideRouter } from '@angular/router';
import { bootstrapApplication } from '@angular/platform-browser';
import { Component } from '@angular/core';
// Composant simple pour nos tests
@Component({
selector: 'app-user',
template: `
<div class="user-profile">
<h1>User Profile</h1>
<p>User ID: {{ userId() }}</p>
</div>
`
})
export class UserComponent {
// Utilisation des signaux pour récupérer l'ID depuis la route
userId = input<string>();
}
// Configuration des routes de test
const testRoutes = [
{ path: 'users/:id', component: UserComponent },
{ path: 'home', component: HomeComponent },
{ path: '', redirectTo: '/home', pathMatch: 'full' }
];
La configuration du test suit cette structure :
typescript
describe('User Navigation Tests', () => {
beforeEach(async () => {
// Configuration minimale avec bootstrapApplication
await bootstrapApplication(AppComponent, {
providers: [
provideRouter(testRoutes),
// Autres providers nécessaires (HttpClient, etc.)
],
teardown: { destroyAfterEach: true } // OBLIGATOIRE pour nettoyer entre les tests
});
});
it('should navigate to user profile', async () => {
// Création du harness - point d'entrée pour tous nos tests
const harness = await RouterTestingHarness.create();
// Navigation vers la route utilisateur
const userComponent = await harness.navigateByUrl('/users/123', UserComponent);
// Vérifications
expect(userComponent.userId()).toBe('123');
expect(harness.routeNativeElement?.querySelector('h1')?.textContent).toBe('User Profile');
});
});
Point important
Le flag teardown: { destroyAfterEach: true }
est obligatoire ! Sans lui, Angular ne nettoie pas le DOM entre les tests, ce qui peut causer des interférences.
Cas d'usage pratiques
Tester la navigation avec paramètres
Imaginons une application de gestion d'utilisateurs où nous devons tester que les paramètres de route sont correctement transmis :
typescript
// Service pour gérer les utilisateurs
@Injectable({ providedIn: 'root' })
export class UserService {
private users = [
{ id: '123', name: 'Alice Dupont', email: '[email protected]' },
{ id: '456', name: 'Bob Martin', email: '[email protected]' }
];
/**
* Récupère un utilisateur par son ID
* @param id - L'identifiant de l'utilisateur
* @returns L'utilisateur trouvé ou undefined
*/
getUserById(id: string) {
return this.users.find(user => user.id === id);
}
}
// Composant utilisateur avec injection de dépendance moderne
@Component({
selector: 'app-user-detail',
template: `
<div class="user-detail">
@if (user()) {
<h1>{{ user()?.name }}</h1>
<p>Email: {{ user()?.email }}</p>
<p>ID: {{ userId() }}</p>
} @else {
<p>Utilisateur non trouvé</p>
}
</div>
`
})
export class UserDetailComponent {
// Injection moderne avec inject()
private userService = inject(UserService);
// Signal pour l'ID utilisateur depuis la route
userId = input.required<string>();
// Signal calculé pour récupérer l'utilisateur
user = computed(() => {
const id = this.userId();
return id ? this.userService.getUserById(id) : null;
});
}
Test correspondant :
typescript
it('should display user details with correct parameters', async () => {
const harness = await RouterTestingHarness.create();
// Navigation avec paramètre
const component = await harness.navigateByUrl('/users/123', UserDetailComponent);
// Vérification que le paramètre est bien transmis
expect(component.userId()).toBe('123');
// Vérification que les données utilisateur sont affichées
const element = harness.routeNativeElement!;
expect(element.querySelector('h1')?.textContent).toBe('Alice Dupont');
expect(element.textContent).toContain('[email protected]');
});
it('should handle non-existent user', async () => {
const harness = await RouterTestingHarness.create();
const component = await harness.navigateByUrl('/users/999', UserDetailComponent);
expect(component.userId()).toBe('999');
expect(harness.routeNativeElement?.textContent).toContain('Utilisateur non trouvé');
});
Tester les guards de navigation
Les guards sont essentiels pour sécuriser votre application. Voici comment les tester efficacement :
typescript
// Guard pour vérifier l'authentification
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
if (authService.isAuthenticated()) {
return true;
}
// Redirection vers la page de connexion
const router = inject(Router);
return router.parseUrl('/login');
};
// Service d'authentification simple
@Injectable({ providedIn: 'root' })
export class AuthService {
private authenticated = false;
isAuthenticated(): boolean {
return this.authenticated;
}
login(): void {
this.authenticated = true;
}
logout(): void {
this.authenticated = false;
}
}
Tests pour le guard :
typescript
describe('Auth Guard Tests', () => {
let authService: AuthService;
beforeEach(async () => {
await bootstrapApplication(AppComponent, {
providers: [
provideRouter([
{ path: 'admin', component: AdminComponent, canActivate: [authGuard] },
{ path: 'login', component: LoginComponent },
{ path: '', redirectTo: '/login', pathMatch: 'full' }
]),
AuthService
],
teardown: { destroyAfterEach: true }
});
authService = TestBed.inject(AuthService);
});
it('should block access when not authenticated', async () => {
// S'assurer que l'utilisateur n'est pas connecté
authService.logout();
const harness = await RouterTestingHarness.create();
// Tentative de navigation vers une route protégée
await harness.navigateByUrl('/admin');
// Vérification de la redirection vers login
expect(harness.routeNativeElement?.tagName.toLowerCase()).toBe('app-login');
});
it('should allow access when authenticated', async () => {
// Connecter l'utilisateur
authService.login();
const harness = await RouterTestingHarness.create();
// Navigation vers la route protégée
const adminComponent = await harness.navigateByUrl('/admin', AdminComponent);
// Vérification que l'accès est autorisé
expect(adminComponent).toBeDefined();
expect(harness.routeNativeElement?.tagName.toLowerCase()).toBe('app-admin');
});
});
Tester les redirections
Les redirections sont courantes dans les applications web. Voici comment les tester :
typescript
it('should redirect from root to home', async () => {
const harness = await RouterTestingHarness.create();
// Navigation vers la racine
await harness.navigateByUrl('/');
// Vérification de la redirection
expect(harness.routeNativeElement?.tagName.toLowerCase()).toBe('app-home');
});
it('should redirect invalid routes to 404', async () => {
const harness = await RouterTestingHarness.create();
// Navigation vers une route inexistante
await harness.navigateByUrl('/route-inexistante');
// Vérification de la redirection vers 404
expect(harness.routeNativeElement?.textContent).toContain('Page non trouvée');
});
Méthodes avancées
Tester les résolveurs de données
Les résolveurs permettent de charger des données avant l'affichage d'un composant :
typescript
// Résolveur pour charger les données utilisateur
export const userResolver: ResolveFn<User | null> = (route) => {
const userService = inject(UserService);
const userId = route.paramMap.get('id');
if (!userId) {
return null;
}
// Simulation d'un appel HTTP asynchrone
return new Promise(resolve => {
setTimeout(() => {
resolve(userService.getUserById(userId));
}, 100);
});
};
// Configuration de route avec résolveur
const routesWithResolver = [
{
path: 'users/:id',
component: UserDetailComponent,
resolve: { user: userResolver }
}
];
Test du résolveur :
typescript
it('should resolve user data before navigation', async () => {
const harness = await RouterTestingHarness.create();
// La navigation attend automatiquement la résolution des données
const component = await harness.navigateByUrl('/users/123', UserDetailComponent);
// Les données sont déjà disponibles dans le composant
expect(component.user()).toBeDefined();
expect(component.user()?.name).toBe('Alice Dupont');
});
Avantage du RouterTestingHarness
La méthode navigateByUrl()
attend automatiquement que tous les résolveurs se terminent avant de retourner le composant. Plus besoin de gérer manuellement l'asynchrone !
Bonnes pratiques
1. Gardez vos routes de test simples
typescript
// ✅ Bon : routes minimales pour le test
const testRoutes = [
{ path: 'users/:id', component: UserComponent },
{ path: 'home', component: HomeComponent }
];
// ❌ Évitez : importer toutes les routes de l'application
import { routes } from './app.routes'; // Trop lourd pour les tests
2. Utilisez des composants de test dédiés
typescript
// Composant simplifié pour les tests
@Component({
template: `<div>Test User: {{ userId() }}</div>`
})
class TestUserComponent {
userId = input<string>();
}
3. Testez les cas d'erreur
typescript
it('should handle navigation errors gracefully', async () => {
const harness = await RouterTestingHarness.create();
// Test avec un guard qui rejette
await expectAsync(
harness.navigateByUrl('/forbidden-route')
).toBeRejected();
});
4. Vérifiez les effets de bord
typescript
it('should update browser URL after navigation', async () => {
const harness = await RouterTestingHarness.create();
await harness.navigateByUrl('/users/123');
// Vérification que l'URL du navigateur est mise à jour
expect(TestBed.inject(Location).path()).toBe('/users/123');
});