Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Définir un provider Angular

Un provider prend cinq formes : la classe seule, useClass, useValue, useFactory ou useExisting. Les types d’Angular permettent de détecter les erreurs dès la compilation. L’instance est créée à la pre...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Un provider prend cinq formes : la classe seule, useClass, useValue, useFactory ou useExisting. Les types d’Angular permettent de détecter les erreurs dès la compilation. L’instance est créée à la première demande, une fois par injecteur, et détruite avec lui. Un jeton peut fournir une fonction ou un signal ; ses dépendances se lisent dans la fabrique. Les valeurs propres à chaque environnement passent par les fichiers d’environnement et un jeton. Le modèle provideXxx(withXxx()) rend la configuration lisible et extensible ; la fonction doit vérifier ses options et lever des erreurs claires. Injector.create() assemble des classes ordinaires grâce à deps, et TestBed.inject() teste un provider sans composant. Le cours se termine par 7 activités corrigées et les erreurs fréquentes.
  1. Objectifs

    • Connaître les cinq formes d'un provider et leurs types TypeScript
    • Savoir quand une instance fournie est créée et détruite
    • Fournir autre chose qu'un service : une fonction, un signal, une valeur d'environnement
    • Écrire des providers configurables sur le modèle provideXxx(withXxx(), …)
    • Valider les options d'un provider pour obtenir des erreurs claires
    • Créer un injecteur à la main avec Injector.create()
    • Organiser et tester ses providers
  1. Présentation

    • Le chapitre précédent a montré où placer les providers et les quatre recettes useClass, useValue, useFactory et useExisting. Celui-ci s'intéresse à leur écriture: comment définir des providers justes, bien typés, faciles à configurer et à tester.
    • On y apprend aussi à fournir ce qui n'est pas une classe (une fonction, un signal, une URL qui change selon l'environnement), et à construire ses propres fonctions provideXxx(), comme celles d'Angular.
  • Pour suivre ce chapitre, continuez dans le projet premiers-pas-angular et créez un dossier src/app/provider/. Tous les exemples ont été compilés et testés avec Angular 22.2 (composants OnPush par défaut, sans Zone.js).
  1. Les cinq formes d'un provider

    • Un provider peut s'écrire de cinq façons. Angular fournit un type pour chacune ; toutes sont des Provider:
      • TypeProvider: la classe seule, forme courte de { provide: X, useClass: X };
      • ClassProvider: useClass, Angular crée l'instance ;
      • ValueProvider: useValue, la valeur est donnée telle quelle ;
      • FactoryProvider: useFactory, la fonction est appelée une fois ;
      • ExistingProvider: useExisting, un alias vers un autre jeton.
import {
  ClassProvider, Component, ExistingProvider, FactoryProvider, Injectable, InjectionToken,
  Provider, TypeProvider, ValueProvider, inject,
} from '@angular/core';

@Injectable()
export class Horaires {
  readonly ouverture: string = '08:00';
}

@Injectable()
export class HorairesEte extends Horaires {
  override readonly ouverture = '07:00';
}

export const SALLE = new InjectionToken<string>('SALLE');
export const CAPACITE = new InjectionToken<number>('CAPACITE');
export const RESUME = new InjectionToken<string>('RESUME');
export const LIEU = new InjectionToken<string>('LIEU');

// Les cinq formes, chacune avec son type
const forme1: TypeProvider = Horaires;                                        // la classe seule
const forme2: ClassProvider = { provide: Horaires, useClass: HorairesEte };   // une autre classe
const forme3: ValueProvider = { provide: SALLE, useValue: 'Salle B12' };      // une valeur
const forme4: FactoryProvider = {                                             // une fonction
  provide: RESUME,
  useFactory: () => `${inject(SALLE)} · ${inject(CAPACITE)} places · ouverture ${inject(Horaires).ouverture}`,
};
const forme5: ExistingProvider = { provide: LIEU, useExisting: SALLE };       // un alias

// Un tableau de providers peut contenir d'autres tableaux : Angular les aplatit
const capacite: Provider[] = [{ provide: CAPACITE, useValue: 24 }];
export const PROVIDERS_SALLE: Provider[] = [forme1, forme2, forme3, forme4, forme5, capacite];

@Component({
  selector: 'app-formes',
  providers: PROVIDERS_SALLE,
  template: `
    <p>{{ resume }}</p>
    <p>Lieu (alias de SALLE) : {{ lieu }}</p>
  `,
})
export class Formes {
  protected readonly resume = inject(RESUME);
  protected readonly lieu = inject(LIEU);
}

Résultat : le résumé fabriqué et l'alias

  • forme1 fournit Horaires, puis forme2 le redéfinit : c'est le dernier qui gagne, d'où « ouverture 07:00 ».
  • capacite est un tableau placé dans le tableau PROVIDERS_SALLE: Angular aplatit les tableaux imbriqués. C'est ce qui permet à une fonction de renvoyer plusieurs providers d'un coup.
  • Typer ses providers protège des fautes de frappe. { provide: Horaires, useclass: HorairesEte } est refusé : TS2353: Object literal may only specify known properties, and 'useclass' does not exist in type 'Provider'. Sans provide: TS2322: Type '{ useClass: typeof HorairesEte; }' is not assignable to type 'Provider'.

Les cinq formes d'un provider et leurs types

  1. Quand l'instance est-elle créée ?

    • Déclarer un provider ne crée rien. Angular crée l'instance à la première demande (inject()), la garde, et renvoie la même à chaque demande suivante dans le même injecteur. Quand cet injecteur est détruit, il appelle ngOnDestroy() sur les instances qu'il a créées.
    • Dans l'exemple, deux fenêtres fournissent Panneau; une seule l'injecte :
import { Component, DestroyRef, Injectable, OnDestroy, inject, signal } from '@angular/core';

// Un journal partagé pour observer les créations et destructions
export const evenements = signal<string[]>([]);
const noter = (m: string) => evenements.update((l) => [...l, m]);

let numero = 0;

@Injectable()
export class Panneau implements OnDestroy {
  readonly id = ++numero;
  constructor() { noter(`Panneau n° ${this.id} créé`); }
  // Appelée quand l'injecteur qui l'a créé est détruit
  ngOnDestroy() { noter(`Panneau n° ${this.id} détruit`); }
}

@Component({
  selector: 'app-fenetre',
  providers: [Panneau],
  template: `<span>Fenêtre</span>`,
})
export class Fenetre {
  // Pas d'inject(Panneau) ici : le provider existe mais n'est pas utilisé
}

@Component({
  selector: 'app-fenetre-active',
  providers: [Panneau],
  template: `<span>Fenêtre active (panneau n° {{ panneau.id }})</span>`,
})
export class FenetreActive {
  protected readonly panneau = inject(Panneau);
  constructor() {
    inject(DestroyRef).onDestroy(() => noter('FenetreActive détruite'));
  }
}

@Component({
  selector: 'app-duree-de-vie',
  imports: [Fenetre, FenetreActive],
  template: `
    <button (click)="visible.set(!visible())">{{ visible() ? 'Fermer' : 'Ouvrir' }} les fenêtres</button>
    @if (visible()) {
      <div><app-fenetre /> · <app-fenetre-active /></div>
    }
    <ol>@for (e of evenements(); track $index) { <li>{{ e }}</li> }</ol>
  `,
})
export class DureeDeVie {
  protected readonly visible = signal(false);
  protected readonly evenements = evenements;
}

Résultat : création et destruction des panneaux

(Ouvrir)    Panneau n° 1 créé            ← un seul : Fenetre ne l'injecte pas
(Fermer)    Panneau n° 1 détruit
            FenetreActive détruite
(Ouvrir)    Panneau n° 2 créé            ← nouvel injecteur, nouvelle instance
  • Fenetre déclare providers: [Panneau] mais ne l'injecte jamais : aucune instance n'est créée. Un provider inutilisé ne coûte rien.
  • Le provider d'un composant vit et meurt avec ce composant. À la racine, l'instance vit jusqu'à la fin de l'application ; dans un injecteur créé à la main, jusqu'à son destroy().
  • Dans une fabrique, on obtient le même nettoyage avec inject(DestroyRef).onDestroy(): il s'exécute à la destruction de l'injecteur qui a appelé la fabrique.

La durée de vie d'une instance fournie

  1. Fournir une fonction

    • Un jeton peut fournir une simple fonction: un format, un calcul, une règle. Le composant l'appelle sans savoir d'où elle vient, et on la remplace aussi facilement qu'un service :
import { Component, InjectionToken, inject } from '@angular/core';

// Le type d'une fonction de formatage
export type FormatPrix = (montant: number) => string;

// Le jeton fournit une FONCTION, avec une version par défaut
export const FORMAT_PRIX = new InjectionToken<FormatPrix>('FORMAT_PRIX', {
  factory: () => (m) => `${m.toFixed(3).replace('.', ',')} DT`,
});

@Component({
  selector: 'app-ticket',
  template: `
    @for (l of lignes; track l.nom) {
      <div>{{ l.nom }} : {{ format(l.prix) }}</div>
    }
    <strong>Total : {{ format(total) }}</strong>
  `,
})
export class Ticket {
  protected readonly format = inject(FORMAT_PRIX);
  protected readonly lignes = [{ nom: 'Cahier', prix: 2.5 }, { nom: 'Stylo', prix: 0.75 }];
  protected readonly total = this.lignes.reduce((t, l) => t + l.prix, 0);
}

@Component({
  selector: 'app-ticket-euro',
  imports: [Ticket],
  providers: [{
    provide: FORMAT_PRIX,
    useValue: (m: number) => new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(m),
  }],
  template: `<app-ticket />`,
})
export class TicketEuro {}

@Component({
  selector: 'app-fonction',
  imports: [Ticket, TicketEuro],
  template: `<h4>Format par défaut</h4><app-ticket /><h4>Format remplacé</h4><app-ticket-euro />`,
})
export class Fonction {}

Résultat : les mêmes prix en dinars puis en euros

  • Le type FormatPrix décrit la fonction ; InjectionToken<FormatPrix> garantit que inject(FORMAT_PRIX) renvoie bien une fonction de ce type.
  • Piège : appeler inject() dans la fonction fournie. Avec useValue: (m) => m.toLocaleString(inject(LOCALE_ID)), la fonction est appelée plus tard, par le template : NG0203: The `InjectionToken LocaleId` token injection failed. `inject()` function must be called from an injection context …
  • La bonne écriture lit les dépendances dans une fabrique, puis renvoie la fonction : useFactory: () => { const l = inject(LOCALE_ID); return (m) => m.toLocaleString(l); }.

Fournir une fonction, et le piège de inject() tardif

  1. Fournir un signal

    • La fabrique d'un jeton peut aussi renvoyer un signal. Comme le jeton est fourni à la racine, la fabrique n'est appelée qu'une fois: un seul écouteur, partagé par tous les composants.
import { Component, DOCUMENT, DestroyRef, InjectionToken, Signal, inject, signal } from '@angular/core';

// Un jeton qui fournit un SIGNAL : la largeur de la fenêtre, partagée par toute l'application
export const LARGEUR_FENETRE = new InjectionToken<Signal<number>>('LARGEUR_FENETRE', {
  factory: () => {
    const fenetre = inject(DOCUMENT).defaultView!;
    const largeur = signal(fenetre.innerWidth);
    const mesurer = () => largeur.set(fenetre.innerWidth);
    fenetre.addEventListener('resize', mesurer);
    // La fabrique tourne dans l'injecteur racine : ce nettoyage aura lieu à sa destruction
    inject(DestroyRef).onDestroy(() => fenetre.removeEventListener('resize', mesurer));
    console.log('LARGEUR_FENETRE : un seul écouteur créé');
    return largeur.asReadonly();
  },
});

@Component({
  selector: 'app-largeur',
  template: `<span>{{ largeur() }} px</span>`,
})
export class Largeur {
  protected readonly largeur = inject(LARGEUR_FENETRE);
}

@Component({
  selector: 'app-jeton-signal',
  imports: [Largeur],
  template: `
    <p>En-tête : <app-largeur /></p>
    <p>Menu : <app-largeur /></p>
    <p>Pied de page : <app-largeur /></p>
  `,
})
export class JetonSignal {}

Résultat : trois composants lisent la même largeur

LARGEUR_FENETRE : un seul écouteur créé      ← une seule fois, pour les trois composants
(fenêtre réduite à 600 px)
En-tête : 600 px · Menu : 600 px · Pied de page : 600 px
  • L'activité 2 du chapitre « Injecter un service » faisait la même chose avec une fonction injectLargeurFenetre(), mais chaque appel ajoutait un écouteur. Avec un jeton, il n'y en a qu'un.
  • La fabrique renvoie asReadonly(): les composants lisent la largeur mais ne peuvent pas la modifier.
  • Dans un test, on remplace le jeton par un signal fixe : { provide: LARGEUR_FENETRE, useValue: signal(375) }.

Fournir un signal partagé par un jeton

  1. Des providers selon l'environnement

    • L'adresse du serveur n'est pas la même en développement et en production. Le CLI sait choisir un fichier différent à la compilation. On crée les fichiers d'environnement avec :
$ ng generate environments
CREATE src/environments/environment.ts (31 bytes)
CREATE src/environments/environment.development.ts (31 bytes)
UPDATE angular.json (2155 bytes)
// Utilisé par ng build (configuration production)
export const environment = {
  production: true,
  apiUrl: 'https://api.apcpedagogie.com',
  journalDetaille: false,
};
// Remplace environment.ts avec ng serve (configuration development)
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000',
  journalDetaille: true,
};
  • Puis on expose chaque valeur par un jeton :
import { Component, InjectionToken, inject } from '@angular/core';
import { environment } from '../../environments/environment';

// Les valeurs viennent du fichier d'environnement choisi à la compilation
export const API_URL = new InjectionToken<string>('API_URL', {
  factory: () => environment.apiUrl,
});
export const JOURNAL_DETAILLE = new InjectionToken<boolean>('JOURNAL_DETAILLE', {
  factory: () => environment.journalDetaille,
});

@Component({
  selector: 'app-environnement-api',
  template: `
    <p>API : <code>{{ api }}</code></p>
    <p>Journal détaillé : {{ detaille ? 'oui' : 'non' }}</p>
  `,
})
export class EnvironnementApi {
  protected readonly api = inject(API_URL);
  protected readonly detaille = inject(JOURNAL_DETAILLE);
}

Résultat avec ng serve : l'API locale

$ ng build
$ grep -o "https://api.apcpedagogie.com\|http://localhost:3000" dist/syntaxe-angular/browser/*.js
https://api.apcpedagogie.com          ← seule l'URL de production est dans le build
  • angular.json contient maintenant un fileReplacements: en configuration development (ng serve), environment.ts est remplacé par environment.development.ts.
  • Les composants injectent API_URL, jamais environment directement : la valeur reste remplaçable dans un test ou dans une partie de l'application.
  • Le fichier d'environnement est compilé dans l'application: il ne doit jamais contenir de mot de passe ou de clé secrète.

Des providers selon l'environnement

  1. Des providers configurables : provideXxx(withXxx())

    • Angular configure ses fonctionnalités avec un modèle très lisible : provideRouter(routes, withHashLocation()), provideHttpClient(withFetch(), withInterceptors([…])). Chaque fonction withXxx() apporte une option ; provideXxx() assemble le tout.
    • On peut écrire le même modèle pour ses propres services. Chaque fonctionnalité ajoute un objet d'options à un jeton multi; une fabrique les fusionne avec les valeurs par défaut :
import {
  EnvironmentProviders, InjectionToken, Provider, Service, inject, makeEnvironmentProviders, signal,
} from '@angular/core';

export interface OptionsNotifications {
  duree: number;               // en millisecondes
  position: 'haut' | 'bas';
  maximum: number;
}

const OPTIONS_PAR_DEFAUT: OptionsNotifications = { duree: 3000, position: 'bas', maximum: 3 };

// Chaque « fonctionnalité » apporte une partie des options
const FONCTIONNALITES = new InjectionToken<Partial<OptionsNotifications>[]>('FONCTIONNALITES');
export const OPTIONS_NOTIFICATIONS = new InjectionToken<OptionsNotifications>('OPTIONS_NOTIFICATIONS');

export interface FonctionnaliteNotifications {
  readonly providers: Provider[];
}

function fonctionnalite(options: Partial<OptionsNotifications>): FonctionnaliteNotifications {
  return { providers: [{ provide: FONCTIONNALITES, multi: true, useValue: options }] };
}

export function withDuree(ms: number): FonctionnaliteNotifications {
  if (ms < 500) {
    throw new Error(`withDuree(${ms}) : la durée doit être d'au moins 500 ms.`);
  }
  return fonctionnalite({ duree: ms });
}

export function withPosition(position: 'haut' | 'bas'): FonctionnaliteNotifications {
  return fonctionnalite({ position });
}

export function withMaximum(maximum: number): FonctionnaliteNotifications {
  return fonctionnalite({ maximum });
}

// Le point d'entrée : provideNotifications(withDuree(5000), withPosition('haut'))
export function provideNotifications(...fonctionnalites: FonctionnaliteNotifications[]): EnvironmentProviders {
  return makeEnvironmentProviders([
    { provide: FONCTIONNALITES, multi: true, useValue: {} }, // le tableau existe même sans fonctionnalité
    ...fonctionnalites.map((f) => f.providers),
    {
      provide: OPTIONS_NOTIFICATIONS,
      useFactory: () => Object.assign({}, OPTIONS_PAR_DEFAUT, ...inject(FONCTIONNALITES)),
    },
    Notifications,
  ]);
}

@Service({ autoProvided: false })
export class Notifications {
  readonly options = inject(OPTIONS_NOTIFICATIONS);
  private readonly _messages = signal<string[]>([]);
  readonly messages = this._messages.asReadonly();

  afficher(message: string) {
    // on garde au plus « maximum » messages
    this._messages.update((l) => [...l, message].slice(-this.options.maximum));
    setTimeout(() => this._messages.update((l) => l.filter((m) => m !== message)), this.options.duree);
  }
}
import {
  Component, DestroyRef, EnvironmentInjector, createEnvironmentInjector, inject, input,
} from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { Notifications, provideNotifications, withDuree, withMaximum, withPosition } from './notifications';

@Component({
  selector: 'app-boite-notifications',
  template: `
    <button (click)="envoyer()">Notifier</button>
    <small>durée {{ notif.options.duree }} ms · position {{ notif.options.position }} · max {{ notif.options.maximum }}</small>
    <div class="pile" [class.haut]="notif.options.position === 'haut'">
      @for (m of notif.messages(); track m) { <div class="toast">{{ m }}</div> }
    </div>
  `,
  styles: `.pile { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
    .pile.haut { flex-direction: column-reverse; }
    .toast { background: #1e293b; color: #fff; padding: 4px 10px; border-radius: 6px; width: fit-content; }`,
})
export class BoiteNotifications {
  protected readonly notif = inject(Notifications);
  private n = 0;
  protected envoyer() { this.notif.afficher(`Message ${++this.n}`); }
}

// Pour la démonstration : deux configurations dans deux injecteurs d'environnement
@Component({
  selector: 'app-zone-notifications',
  imports: [NgComponentOutlet],
  template: `
    <h4>provideNotifications()</h4>
    <ng-container *ngComponentOutlet="boite; environmentInjector: simple" />
    <h4>provideNotifications(withDuree(8000), withPosition('haut'), withMaximum(2))</h4>
    <ng-container *ngComponentOutlet="boite; environmentInjector: reglee" />
  `,
})
export class ZoneNotifications {
  protected readonly boite = BoiteNotifications;
  private readonly parent = inject(EnvironmentInjector);
  protected readonly simple = createEnvironmentInjector([provideNotifications()], this.parent);
  protected readonly reglee = createEnvironmentInjector(
    [provideNotifications(withDuree(8000), withPosition('haut'), withMaximum(2))],
    this.parent,
  );
  constructor() {
    inject(DestroyRef).onDestroy(() => { this.simple.destroy(); this.reglee.destroy(); });
  }
}

Résultat : deux configurations du même service

(4 clics sur chaque bouton)
provideNotifications()             Message 2 · Message 3 · Message 4     (3 au plus, en bas)
provideNotifications(withDuree(8000), withPosition('haut'), withMaximum(2))
                                   Message 4 · Message 3                 (2 au plus, en haut)
(3,4 s plus tard)                  la première pile est vide, la seconde reste affichée
  • { provide: FONCTIONNALITES, multi: true, useValue: {} } garantit que le tableau existe même quand aucune fonctionnalité n'est passée.
  • Object.assign() applique les options dans l'ordre : en cas de doublon (withPosition('bas'), withPosition('haut')), la dernière gagne.
  • Notifications est en autoProvided: false: impossible de l'utiliser sans passer par provideNotifications(), donc sans options.
  • Dans l'application, l'appel se place dans app.config.ts; l'exemple utilise deux injecteurs d'environnement pour comparer deux configurations sur la même page.

Le modèle provideXxx(withXxx())

  1. Valider les options

    • Un jeton ne vérifie pas la valeur qu'on lui donne. { provide: CAPACITE, useValue: '24' } sur un InjectionToken<number> compile sans erreur, et {{ c + 1 }} affiche… « 241 ».
    • Une fonction provideXxx() ou withXxx() est l'endroit idéal pour vérifier ses paramètres et lever une erreur claire, dès le démarrage. C'est ce que fait withDuree() plus haut :
provideNotifications(withDuree(100))
ERROR Error: withDuree(100) : la durée doit être d'au moins 500 ms.
  • Le message nomme la fonction, la valeur reçue et la règle : on corrige en quelques secondes.
  • Le paramètre typé (ms: number, position: 'haut' | 'bas') élimine déjà beaucoup d'erreurs à la compilation ; la vérification couvre ce que les types ne peuvent pas dire (« au moins 500 »).

Valider les options d'un provider

  1. Créer un injecteur à la main : Injector.create()

    • Injector.create() construit un injecteur à partir d'une liste de providers. Il sert à assembler des classes ordinaires, sans décorateur, par exemple une bibliothèque de calcul. Comme ces classes n'ont pas de métadonnées, on indique leurs dépendances avec deps, dans l'ordre des paramètres du constructeur :
import { Component, InjectionToken, Injector, inject, signal } from '@angular/core';
import { FORMAT_PRIX } from './fonction';

export const TAUX_REMISE = new InjectionToken<number>('TAUX_REMISE');

// Une classe ordinaire, sans décorateur
export class Caisse {
  constructor(
    private readonly taux: number,
    private readonly format: (m: number) => string,
  ) {}
  encaisser(montant: number) {
    return `${this.format(montant)} − ${this.taux * 100} % = ${this.format(montant * (1 - this.taux))}`;
  }
}

@Component({
  selector: 'app-injecteur-manuel',
  template: `
    @for (r of resultats(); track $index) { <div>{{ r }}</div> }
    <button (click)="soldes()">Caisse des soldes</button>
  `,
})
export class InjecteurManuel {
  private readonly injector = inject(Injector);
  protected readonly resultats = signal<string[]>([]);

  protected soldes() {
    // Un injecteur créé à la main, enfant de celui du composant
    const injecteur = Injector.create({
      name: 'Soldes',
      parent: this.injector,
      providers: [
        { provide: TAUX_REMISE, useValue: 0.3 },
        // Injector.create exige des providers « statiques » : deps liste les paramètres
        { provide: Caisse, useClass: Caisse, deps: [TAUX_REMISE, FORMAT_PRIX] },
      ],
    });
    const caisse = injecteur.get(Caisse);
    this.resultats.update((l) => [...l, caisse.encaisser(40), caisse.encaisser(12.5)]);
  }
}

Résultat : la caisse des soldes

  • parent: this.injector rend visibles les providers de l'application : FORMAT_PRIX est trouvé à la racine avec sa fabrique par défaut.
  • Si l'on oublie deps, TypeScript ne dit rien, mais à l'exécution : NG0204: Can't resolve all parameters for Caisse: (?, ?). Les deux ? sont les deux paramètres inconnus.
  • Dans un composant ou un service Angular, on n'a pas besoin de Injector.create(): providers et inject() suffisent.

Créer un injecteur à la main avec Injector.create

  1. Organiser ses providers

    • Dans une vraie application, chaque fonctionnalité exporte sa fonction provideXxx(), souvent dans un fichier xxx.providers.ts, et les jetons partagés sont regroupés. app.config.ts se lit alors comme une table des matières :

Organiser les providers d'un projet

  1. Tester ses providers

    • Un provider se teste sans composant: on configure TestBed avec la fonction à tester, puis TestBed.inject() lit ce qu'elle produit.
import { TestBed } from '@angular/core/testing';
import {
  Notifications, OPTIONS_NOTIFICATIONS, provideNotifications, withDuree, withMaximum, withPosition,
} from './notifications';

describe('provideNotifications', () => {
  it('utilise les valeurs par défaut', () => {
    TestBed.configureTestingModule({ providers: [provideNotifications()] });
    expect(TestBed.inject(OPTIONS_NOTIFICATIONS)).toEqual({ duree: 3000, position: 'bas', maximum: 3 });
  });

  it('combine les fonctionnalités', () => {
    TestBed.configureTestingModule({
      providers: [provideNotifications(withPosition('haut'), withDuree(8000))],
    });
    expect(TestBed.inject(OPTIONS_NOTIFICATIONS)).toEqual({ duree: 8000, position: 'haut', maximum: 3 });
  });

  it('refuse une durée trop courte', () => {
    expect(() => withDuree(100)).toThrowError("withDuree(100) : la durée doit être d'au moins 500 ms.");
  });

  it('garde au plus « maximum » messages', () => {
    TestBed.configureTestingModule({ providers: [provideNotifications(withMaximum(2))] });
    const notif = TestBed.inject(Notifications);
    notif.afficher('a');
    notif.afficher('b');
    notif.afficher('c');
    expect(notif.messages()).toEqual(['b', 'c']);
  });
});
$ ng test --include src/app/provider/notifications.spec.ts
 ✓ provideNotifications > utilise les valeurs par défaut
 ✓ provideNotifications > combine les fonctionnalités
 ✓ provideNotifications > refuse une durée trop courte
 ✓ provideNotifications > garde au plus « maximum » messages

      Tests  4 passed (4)

Tester ses providers avec TestBed.inject

  1. Aide-mémoire

Aide-mémoire : définir un provider Angular

  1. Activité 1 : un agenda aux formats interchangeables

    • Créer le type FormatDate ((d: Date) => string) et le jeton FORMAT_DATE, dont la fabrique renvoie le format long : « lundi 2 novembre 2026 ».
    • Créer le jeton AUJOURDHUI (Date, fabrique : new Date()).
    • Le composant Agenda affiche trois événements avec la fonction injectée. L'afficher trois fois : format par défaut, format court (useValue: « 02/11/2026 ») et format relatif (useFactory: « demain », « dans 8 jours »), avec AUJOURDHUI fixé au 1er novembre 2026.

Résultat attendu de l'activité 1 : trois agendas

  • Vérifier : Examen Angular → « lundi 2 novembre 2026 », « 02/11/2026 », « demain » ; Vacances → « dans 49 jours ».
  • Pourquoi le format relatif lit-il AUJOURDHUI dans la fabrique et non dans la fonction renvoyée ?
  1. Activité 2 : un assistant qui oublie, un assistant qui se souvient

    • Créer le service Reponses (@Injectable()) : un numéro d'instance, une Map des réponses, et un journal partagé qui note « Reponses n° x créé » à la construction et « … détruit » dans ngOnDestroy().
    • Créer le composant Etape (entrée question) : un champ qui lit et écrit sa réponse dans Reponses.
    • Version 1 : chaque étape est un composant qui fournit Reponses. Version 2 : l'assistant fournit Reponses une seule fois. Les deux affichent une étape à la fois avec @switch et deux boutons ◀ ▶.

Résultat attendu de l'activité 2 : deux assistants et leur journal

  • Vérifier : taper « Amel », aller à l'étape suivante puis revenir. La version 1 a oublié la réponse (« n° 1 détruit »), la version 2 l'a gardée (une seule instance, jamais détruite).
  • Où faut-il donc fournir un service dont les données doivent survivre à un composant ?
  1. Activité 3 : un bandeau hors ligne

    • Créer le jeton EN_LIGNE (Signal<boolean>) : sa fabrique part de navigator.onLine, écoute les événements online et offline de la fenêtre et les retire avec DestroyRef.
    • BandeauReseau affiche « ⚠ Hors ligne : … » quand le réseau est coupé ; EnvoiDevoir désactive son bouton « Envoyer le devoir » hors ligne.

Résultat attendu de l'activité 3 : hors ligne

  • Vérifier dans les outils de développement (onglet Réseau, « Hors ligne ») : le bandeau apparaît, le bouton se grise ; au retour du réseau, l'envoi fonctionne.
  • Combien d'écouteurs online sont installés si dix composants injectent EN_LIGNE?
  1. Activité 4 : provideJournal(withXxx())

    • Sur le modèle de provideNotifications(), écrire provideJournal(...fonctionnalites) et trois fonctionnalités : withNiveau('info' | 'avertissement' | 'erreur'), withHorodatage() et withPrefixe(prefixe). Valeurs par défaut : niveau 'info', pas d'heure, préfixe 'APP'.
    • withPrefixe() refuse un préfixe qui n'est pas fait de 2 à 8 lettres majuscules.
    • Le service Journal (autoProvided: false) ignore les messages sous le niveau choisi et écrit « [APP] ERREUR serveur injoignable ».
    • Afficher deux consoles (trois boutons info / avertissement / erreur) dans deux injecteurs d'environnement : provideJournal() et provideJournal(withNiveau('avertissement'), withHorodatage(), withPrefixe('NOTES')).

Résultat attendu de l'activité 4 : deux journaux

  • Vérifier : après un clic sur chaque bouton, le premier journal a 3 lignes, le second 2 (l'info est ignorée), avec l'heure et « [NOTES] ».
  • Que donne withPrefixe('notes')? À quel moment l'erreur apparaît-elle ?
  1. Activité 5 : provideMonnaie() avec vérification

    • Créer l'interface Monnaie (code, decimales) et le jeton MONNAIE (dinar, 3 décimales par défaut).
    • Écrire provideMonnaie(code, decimales = 2): elle refuse un code qui n'est pas fait de 3 lettres majuscules, et un nombre de décimales hors de 0 à 3, avec un message qui cite la valeur reçue.
    • Le composant Tarifs formate deux prix avec Intl.NumberFormat. L'afficher avec la monnaie par défaut, avec provideMonnaie('EUR') et avec provideMonnaie('JPY', 0).

Résultat attendu de l'activité 5 : trois monnaies

  • Vérifier : 450,000 TND, 450,00 €, 450 JPY ; et 120,5 devient 121 JPY sans décimale.
  • Essayer provideMonnaie('dinar'): lire le message. Qu'aurait affiché l'application sans cette vérification ?
  1. Activité 6 : un générateur de rapports avec Injector.create()

    • Créer des classes sans décorateur: SourceNotes (trois notes d'élèves), MiseEnForme (abstraite) avec MiseEnFormeTexte et MiseEnFormeTableau, et Rapport, qui reçoit par son constructeur une source, une mise en forme et un seuil, et produit les lignes puis « 2/3 admis (seuil 10) ».
    • Le composant GenerateurRapport a deux boutons. Chacun crée un injecteur avec Injector.create() (le seuil vient d'un jeton SEUIL, la mise en forme dépend du bouton) puis affiche injecteur.get(Rapport).produire().

Résultat attendu de l'activité 6 : le rapport en tableau

  • Vérifier : « Amel a obtenu 15/20 » en texte, « | Amel | 15 | » en tableau, et « 2/3 admis (seuil 10) » dans les deux cas.
  • Retirer le deps de Rapport: quel message obtient-on, et pourquoi la compilation ne l'a-t-elle pas signalé ?
  1. Activité 7 : tester provideJournal() et provideMonnaie()

    • Écrire providers.spec.ts, sans aucun composant, avec TestBed.inject():
      • provideJournal(): les options par défaut ; trois fonctionnalités dont deux withPrefixe() (la dernière gagne) ; avec withNiveau('erreur'), seul le message d'erreur est gardé ; withPrefixe('notes') lève une erreur ;
      • provideMonnaie(): la monnaie demandée ; le dinar par défaut sans provider ; les deux erreurs de validation.
$ ng test --include src/app/act-provider/providers.spec.ts
 ✓ provideJournal > a des options par défaut
 ✓ provideJournal > applique les fonctionnalités dans l'ordre
 ✓ provideJournal > ignore les messages sous le niveau choisi
 ✓ provideJournal > refuse un préfixe invalide
 ✓ provideMonnaie > fournit la monnaie demandée
 ✓ provideMonnaie > garde le dinar par défaut
 ✓ provideMonnaie > refuse un code ou des décimales invalides

      Tests  7 passed (7)
  • Vérifier : 7 tests réussis.
  • Pourquoi ces tests sont-ils plus rapides et plus simples que des tests de composants ?
  • Les corrigés des sept activités, testés avec Angular 22, sont fournis dans l'archive de la leçon (dossier corriges/).
  1. Erreurs fréquentes

Message ou symptôme Cause Solution
TS2353: Object literal may only specify known properties, and 'useclass' does not exist in type 'Provider'. Faute de frappe dans une clé du provider useClass, useValue, useFactory, useExisting
TS2322: Type '{ useClass: … }' is not assignable to type 'Provider'. Clé provide oubliée Ajouter provide: X
{{ c + 1 }} affiche « 241 » useValue: '24' sur un jeton numérique : non vérifié Corriger la valeur ; passer par une fonction provideXxx() typée
NG0203: The `InjectionToken LocaleId` token injection failed … inject() dans la fonction fournie, appelée plus tard Lire la dépendance dans une fabrique, puis renvoyer la fonction
NG0204: Can't resolve all parameters for Caisse: (?, ?). Injector.create() sans deps deps: [JETON_1, JETON_2] dans l'ordre du constructeur
ERROR Error: withDuree(100) : la durée doit être d'au moins 500 ms. Option refusée par la validation Corriger l'appel : c'est le but de la vérification
Mauvaise implémentation utilisée Le même jeton est fourni deux fois : le dernier gagne Supprimer le doublon
Les données disparaissent en changeant d'écran Service fourni par un composant détruit Le fournir plus haut (parent, route ou racine)
Un écouteur par composant Fonction injectXxx() appelée dans chaque composant Un jeton avec fabrique à la racine
Une URL de développement en production import du mauvais fichier, ou fileReplacements absent Importer environment.ts; vérifier angular.json
  1. À retenir

    • Un provider a cinq formes : la classe seule, useClass, useValue, useFactory, useExisting; les types Provider, ClassProvider… évitent les fautes de frappe.
    • L'instance est créée à la première demande, une fois par injecteur, et détruite avec lui (ngOnDestroy, DestroyRef).
    • Un jeton peut fournir une fonction ou un signal ; les dépendances se lisent dans la fabrique, jamais dans la fonction renvoyée.
    • Les valeurs propres à chaque environnement passent par ng generate environments et un jeton avec fabrique.
    • Le modèle provideXxx(withXxx()) rend une configuration lisible, extensible et documentée.
    • Une fonction provideXxx() doit vérifier ses options et lever une erreur claire.
    • Injector.create() assemble des classes ordinaires grâce à deps.
    • TestBed.inject() teste un provider sans composant.
Un bon provider se lit comme une phrase : « pour ce jeton, utilise cette recette ». Bien typé, vérifié et testé, il rend la configuration de l'application aussi fiable que son code.