📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~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() et output() 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 des signal()
  • tu codes beaucoup de moins de fixture.detectChanges()

Dans cet article, on va :

  1. poser un exemple de composant ultra simple
  2. voir la version “Avant” avec fixture.componentRef.setInput(...)
  3. voir la version “Après” avec inputBinding / outputBinding
  4. finir par un avant / après visuel

1. Le composant d’exemple : LikeButton

On va rester simple : un bouton “Like” avec :

  • un input() label pour 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();
  }
}
  • label est un signal input (input('Like'))
  • liked est 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
  • 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’est ComponentFixtureAutoDetect qui 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 detectChanges partout 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 dans bindings
  • tu réduis énormément le bruit :
    • moins de fixture.detectChanges()
    • moins de manipulations directes de l’instance
  • 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

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

EasyAngularKit

Formation complète pour maîtriser Angular et développer des applications web modernes.

Navigation

Contact

Légal

© 2026 Easy Angular Kit. Tous droits réservés.