Live le 29 mai

Watch Party Angular 20 🎉

Rejoignez-nous pour une soirée spéciale dédiée à Angular 20 ! On découvre ensemble les nouveautés, on discute et on réagit en direct.

Programme :

  • 19:45Apéro ! 🍻. Nous parlerons des avancées d'Angular.
  • 20:00Nous suivrons ensemble les nouveautés d'Angular 20.
Rejoindre la soirée
Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

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');
});