~4 min de lecture
Tester ses composants Angular avec inputBinding / outputBinding
et (presque) dire adieu à fixture.detectChanges()
Avec les versions récentes d’Angular (v20+), TestBed.createComponent a beaucoup évolué :
- tu peux désormais lui passer des
bindings - tu peux déclarer des
input()etoutput()de test au moment même où tu crées le composant - tu peux t’appuyer sur
{ provide: ComponentFixtureAutoDetect, useValue: true }pour une détection de changements automatique
En pratique, ça change énormément la façon d’écrire des tests :
- tu déclares les
input()à la création du composant - tu branches les
output()sur des callbacks ou dessignal() - tu codes beaucoup de moins de
fixture.detectChanges()
Dans cet article, on va :
- poser un exemple de composant ultra simple
- voir la version “Avant” avec
fixture.componentRef.setInput(...) - voir la version “Après” avec
inputBinding/outputBinding - finir par un avant / après visuel
1. Le composant d’exemple : LikeButton
On va rester simple : un bouton “Like” avec :
- un
input()labelpour le texte - un
output()likedémis au clic
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-like-button',
template: `
<button type="button" (click)="onClick()">
👍 {{ label() }}
</button>
`,
})
export class LikeButton {
readonly label = input('Like');
readonly liked = output<void>();
onClick() {
this.liked.emit();
}
}
labelest un signal input (input('Like'))likedest un signal output (output<void>())
Tout est prêt pour des tests signal-first.
2. Avant : tests avec fixture.componentRef.setInput(...)
Avant inputBinding / outputBinding, la manière “moderne” de tester un composant standalone était :
- utiliser
fixture.componentRef.setInput(...)pour simuler les@Input - s’abonner manuellement au
output()pour vérifier les émissions - appeler
fixture.detectChanges()à la main aux bons endroits (si on n'est pas en zoneless)
2.1. Tests “avant” sans configureTestingModule
import { TestBed } from '@angular/core/testing';
import { LikeButton } from './like-button';
describe('LikeButton (avant)', () => {
it('affiche le label donné en input', async () => {
const fixture = TestBed.createComponent(LikeButton);
// ✅ Ancienne approche : setInput
fixture.componentRef.setInput('label', 'J’aime');
// 😅 Toujours y penser
fixture.detectChanges();
// quoiqu'il arrive on le garde
await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button')!;
expect(button.textContent).toContain('J’aime');
});
it('émet liked quand on clique', async () => {
const fixture = TestBed.createComponent(LikeButton);
let likedCalled = false;
const sub = fixture.componentInstance.liked.subscribe(() => {
likedCalled = true;
});
fixture.detectChanges();
await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button')!;
button.click();
expect(likedCalled).toBe(true);
});
});
Ça marche, mais :
- tu dois penser à
fixture.detectChanges()avant et après les actions - tu manipules directement l’instance (
componentRef / componentInstance) - tu n’exprimes pas clairement, en haut de ton test,
“voici les inputs” et “voici ce qu’on fait des outputs”
On peut faire mieux, plus proche de la façon dont Angular lui-même “voit” ton composant.
3. Après : inputBinding / outputBinding + signaux
Les nouveaux helpers permettent de décrire les bindings dès la création du composant.
L’idée :
- on crée le composant avec
TestBed.createComponent(…, { bindings: [...] }) - on passe :
- des
inputBinding('label', ...)pour les inputs - des
outputBinding('liked', ...)pour les outputs
- des
- on laisse Angular gérer la détection via
ComponentFixtureAutoDetect - on garde
fixture.whenStable()pour être sûrs que tout est à jour
🧪 Tous les exemples utilisent Vitest.
On suppose ici que ta config Angular + Vitest est déjà en place (setup global).
3.1. Import des helpers
import {
inputBinding,
outputBinding,
signal,
} from '@angular/core';
import {
TestBed,
ComponentFixtureAutoDetect,
} from '@angular/core/testing';
import { vi } from 'vitest';
import { LikeButton } from './like-button.component';
3.2. Tester un input avec inputBinding
it('affiche le label fourni via inputBinding', async () => {
const fixture = TestBed.createComponent(LikeButton, {
providers: [
// Angular se charge des detectChanges
{ provide: ComponentFixtureAutoDetect, useValue: true },
],
bindings: [
// on fournit le label via un signal
inputBinding('label', signal('J’adore')),
],
});
await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button')!;
expect(button.textContent).toContain('J’adore');
});
Remarques :
inputBinding('label', signal('J’adore'))'label'= nom public de l’input (la string doit matcher le nom ou l’alias)signal('J’adore')= valeur fournie à l’input
- pas de
fixture.detectChanges()→ c’estComponentFixtureAutoDetectqui bosse - on garde
await fixture.whenStable()pour laisser Angular finir les mises à jour
Tu peux aussi passer directement une fonction :
bindings: [
inputBinding('label', () => 'J’adore'),
],
3.3. Tester un output avec outputBinding + signal
Pour l’output, on peut brancher directement un signal writable.
it('met à jour un signal quand liked est émis', async () => {
const likedSignal = signal(false);
const spy = vi.spyOn(likedSignal, 'set');
const fixture = TestBed.createComponent(LikeButton, {
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true },
],
bindings: [
// on pourrait aussi passer un getter si besoin
inputBinding('label', () => 'Like'),
// on branche l’output sur une callback qui met le signal à jour
outputBinding('liked', () => likedSignal.set(true)),
],
});
await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button')!;
button.click();
await fixture.whenStable();
expect(spy).toHaveBeenCalledWith(true);
expect(likedSignal()).toBe(true);
});
Ici :
outputBinding('liked', () => likedSignal.set(true)):'liked'= nom de l’output- la callback est appelée à chaque
emit()
vi.spyOn(likedSignal, 'set')permet de vérifier les valeurs reçues- tu peux aussi juste vérifier la valeur finale avec
likedSignal()
Résultat : ton test décrit clairement :
- les entrées : dans
bindings: [ inputBinding(...), ... ] - les sorties : dans
bindings: [ outputBinding(...), ... ]
…sans jamais manipuler directement l'instance du composant.
4. Le combo gagnant : options de createComponent
Avec la nouvelle API de TestBed.createComponent, tu peux tout déclarer au même endroit :
const fixture = TestBed.createComponent(LikeButton, {
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true },
],
bindings: [
inputBinding('label', signal('Like')),
outputBinding('liked', () => { /* ... */
}),
],
});
Ce pattern :
- te rapproche du comportement “réel” de l’application
- évite le bruit de
detectChangespartout dans les specs
5. Section “Avant / Après” à copier-coller
Pour visualiser la différence, voici un petit comparatif.
5.1. Pour les Inputs
Avant
const fixture = TestBed.createComponent(LikeButton);
fixture.componentRef.setInput('label', 'J’aime');
fixture.detectChanges();
Après
const fixture = TestBed.createComponent(LikeButton, {
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true },
],
bindings: [
inputBinding('label', signal('J’adore')),
],
});
await fixture.whenStable();
5.2. Pour les Outputs
Avant
const fixture = TestBed.createComponent(LikeButton);
let likedCalled = false;
const sub = fixture.componentRef.instance.liked.subscribe(() => {
likedCalled = true;
});
fixture.detectChanges();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button')!;
button.click();
fixture.detectChanges();
expect(likedCalled).toBe(true);
sub.unsubscribe();
Après
const liked = signal(false);
const spy = vi.spyOn(liked, 'set');
const fixture = TestBed.createComponent(LikeButton, {
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true },
],
bindings: [
outputBinding('liked', () => liked.set(true)),
],
});
await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button')!;
button.click();
await fixture.whenStable();
expect(spy).toHaveBeenCalledWith(true);
expect(liked()).toBe(true);
🧠 Résumé :
- Avant : on pilotait le composant “de l’intérieur”
> (instance + `setInput` + `detectChanges` + subscriptions)
- Après : on le pilote uniquement de l'extérieur, le reste c'est boîte noire :
> “voici les inputs”, “voici ce qu’on fait des outputs”, déclarés dans `bindings`.
7. Conclusion
Avec inputBinding et outputBinding :
- tes tests deviennent plus déclaratifs :
les inputs et outputs sont visibles en un coup d’œil dansbindings - tu réduis énormément le bruit :
- moins de
fixture.detectChanges() - moins de manipulations directes de l’instance
- moins de
- tu restes aligné avec le modèle signal-first d’Angular moderne
Envie de découvrir EasyAngularKit et son programme ?
→ Découvre EasyAngularKit : https://pim.ms/C31g7p2