We use cookies to enhance your experience on the site
CodeWorlds

Testing Time and Signals

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 and tick - stop the clock and push it forward

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.

waitForAsync - wait for real asynchrony

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.

Testing signals - the easiest thing in Angular

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.

Testing effects

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.

Go to CodeWorlds