Some samurai techniques land late - the blow arrives a second after the swing. How do you test that without actually waiting? In this lesson you will learn the single most useful trick in asynchronous testing: inside a test, you command time. You stop the clock, push it forward by exactly as much as you need, and check the result - all in a fraction of a second.
fakeAsync creates a zone in which time stands still until you push it. tick(ms) moves it forward by hand, by the number of milliseconds you name. Thanks to that, a test of a one-second delay takes a blink instead of a second.1import { fakeAsync, tick, flush } from '@angular/core/testing';
2
3it('should update after delay', fakeAsync(() => {
4 const component = new AsyncComponent();
5
6 component.loadData(); // sets a setTimeout of 1000ms
7 expect(component.data).toBeNull();
8
9 tick(500); // move the clock forward by 500ms
10 expect(component.data).toBeNull(); // too early
11
12 tick(500); // another 500ms - 1000 in total
13 expect(component.data).toBeTruthy(); // ready now
14}));Follow the logic of time here: right after
loadData the data is still empty, because the timer has not fired yet. After the first tick(500) it is still empty - only half a second of virtual time has passed. Only the second tick(500) completes the full second and the timer goes off. Notice what tick does not do: it never waits one real second, it does not run any loop iterations, and it does not cap how long the test may take. It only simulates the passage of virtual time, which is why the test is instant and completely predictable. When a component starts many timers at once and you do not want to count milliseconds, flush() runs every pending timer without you naming a time; for a Promise that is waiting to resolve you reach for flushMicrotasks() instead.Almost every
fakeAsync test repeats the same four steps, and a search field with a 300 ms debounce is the classic example. First you create the spy: const spy = spyOn(component, 'performSearch'). Then you trigger the action: component.onSearchChange('samurai'). Then you move the clock with tick(300). And only then do you assert with expect(spy).toHaveBeenCalledWith('samurai'). The order matters as much as the katana draw - assert before the tick and nothing has happened yet, so the test fails on a debounce that works perfectly well.fakeAsync fakes time. Sometimes, though, you are testing something genuinely asynchronous - a request, for instance - and instead of fast-forwarding you want to wait until everything settles. That is the job of waitForAsync together with whenStable.1import { waitForAsync } from '@angular/core/testing';
2
3it('should fetch data asynchronously', waitForAsync(() => {
4 const fixture = TestBed.createComponent(AsyncHttpComponent);
5 const component = fixture.componentInstance;
6
7 component.fetchData();
8
9 fixture.whenStable().then(() => {
10 fixture.detectChanges();
11 expect(component.data).toBeTruthy();
12 });
13}));The difference is one of attitude:
fakeAsync says "wind the clock forward", while waitForAsync says "wait until every task running in the background has finished" - that is what whenStable reports. Keep the rule simple: a known setTimeout calls for fakeAsync, undefined background work calls for waitForAsync. There is a third, optional technique worth knowing about: RxJS ships marble testing with TestScheduler from rxjs/testing, where you describe a whole stream of emissions as a short string such as 'a-b-c|'. For everyday component tests the two tools above will carry you.After the intricacies of time you can breathe out: signals are trivial to test. A signal is a box holding a value - you set it and you read it, with no asynchrony anywhere in sight.
1import { signal, computed } from '@angular/core';
2
3it('should update and derive signal values', () => {
4 const count = signal(0);
5
6 count.set(5);
7 expect(count()).toBe(5); // read: count()
8
9 const doubled = computed(() => count() * 2);
10 expect(doubled()).toBe(10); // derived automatically
11
12 count.set(3);
13 expect(doubled()).toBe(6); // recomputed by itself
14});The whole craft is
set (write) and () (read). Look closely at the read, because this is where beginners slip: you call the signal like a function. There is no .value property and no .get() method, and passing the signal itself into expect compares the function, not the number - count() is the only correct way to reach the current value in an assertion. When the new value depends on the old one, count.update(v => v + 1) does the same work as reading and setting. computed is nicer still: you never set it by hand, it recalculates from the signals it depends on. The test proves it - you change count and doubled already carries the new value. No tick, no waiting.Inside a component there is one extra step - after changing a signal you call
detectChanges so the template refreshes:1it('should update template when signal changes', () => {
2 component.count.set(42);
3 fixture.detectChanges(); // refresh the view
4 expect(fixture.nativeElement.querySelector('.count').textContent).toContain('42');
5});
6
7it('should react to input signal changes', () => {
8 fixture.componentRef.setInput('name', 'Musashi'); // for input() we use setInput
9 fixture.detectChanges();
10 expect(fixture.nativeElement.textContent).toContain('Musashi');
11});Remember two differences here. A plain signal you set with
.set(), but an input signal created with input() you set through fixture.componentRef.setInput(...), because that value comes from the parent - the component never hands it to itself. And after every change detectChanges copies the value into the template, so only then does it make sense to check the rendered text.effect runs a piece of code on every change of the signals it reads. To test one you need an injection context and a manual run of the pending effects through TestBed.flushEffects.1it('should trigger effect on signal change', () => {
2 let effectCount = 0;
3
4 TestBed.runInInjectionContext(() => {
5 const count = signal(0);
6 effect(() => { count(); effectCount++; }); // the effect reads count
7
8 TestBed.flushEffects();
9 expect(effectCount).toBe(1); // first run
10
11 count.set(1);
12 TestBed.flushEffects();
13 expect(effectCount).toBe(2); // the effect reacted to the change
14 });
15});An effect does not fire the instant a signal changes - Angular batches effects and runs them to its own rhythm, which is why a test forces the run with
TestBed.flushEffects(). The effectCount counter then proves the thing that matters most: the effect ran once on start and a second time after the signal changed. Had the number stayed at one, it would mean the effect never read count and therefore does not track it at all.Take this away from the lesson: in a test you are the master of time -
fakeAsync with tick for timers you know about, waitForAsync for background work you do not. And signals are the easy part of the dojo: set the value, call detectChanges, check the result.