Signalgetriebene Effekte mit setTimeout-basiertem Debouncing sind notorisch schwer zu testen, weil die Cleanup-Funktion des Effekts den veralteten Timer tatsächlich abbrechen muss, statt ihn einfach laufen zu lassen und zu überschreiben. Dieser Test nutzt fakeAsync und tick, um die virtuelle Zeit deterministisch zu steuern: Er lässt 100ms vergehen (bevor das Debounce auslöst), ändert die Query erneut, lässt dann die restlichen 300ms vergehen und prüft, dass nur die Ergebnisse der letzten Query übrig bleiben — ein Beweis, dass der onCleanup-Callback den ersten setTimeout wirklich gelöscht hat, statt nur mit ihm zu wettlaufen.
import { Component, signal, effect, computed } from '@angular/core';import { TestBed, fakeAsync, tick } from '@angular/core/testing';@Component({ selector: 'app-debounced-search', template: '' })class DebouncedSearchComponent {readonly query = signal('');readonly results = signal<string[]>([]);private searchCallCount = 0;constructor() {effect((onCleanup) => {const term = this.query();const handle = setTimeout(() => {this.searchCallCount++;this.results.set(term ? [`${term}-1`, `${term}-2`] : []);}, 300);onCleanup(() => clearTimeout(handle));});}}describe('DebouncedSearchComponent', () => {it('cancels the pending search when the query changes before the debounce fires', fakeAsync(() => {const fixture = TestBed.createComponent(DebouncedSearchComponent);const component = fixture.componentInstance;fixture.detectChanges();component.query.set('ang');tick(100);component.query.set('angular');tick(300);fixture.detectChanges();expect(component.results()).toEqual(['angular-1', 'angular-2']);}));});