📧 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://www.easyangularkit.com/easyangularkit

📧 Reste informé(e) !

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

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.