Kurs Angular · Moduł 11: Testowanie

Testowanie czasu i sygnałów

4 min czytania ·
W tej lekcji4

Niektóre techniki samuraja działają z opóźnieniem - cios pada sekundę po zamachu. Jak to przetestować, nie czekając naprawdę? W tej lekcji poznasz najważniejszą sztuczkę testów asynchronicznych: w teście to Ty władasz czasem. Zatrzymasz zegar, przesuniesz go o tyle, ile chcesz, i sprawdzisz efekt - wszystko w ułamku sekundy.

fakeAsync i tick - zatrzymaj i przesuń zegar

fakeAsync tworzy strefę, w której czas stoi w miejscu, dopóki go nie pchniesz. tick(ms) przesuwa go ręcznie o zadaną liczbę milisekund. Dzięki temu test opóźnienia trwa mgnienie, nie sekundy.

import { fakeAsync, tick, flush } from '@angular/core/testing';

it('should update after delay', fakeAsync(() => {
  const component = new AsyncComponent();

  component.loadData();  // ustawia setTimeout 1000ms
  expect(component.data).toBeNull();

  tick(500);             // przesuń zegar o 500ms
  expect(component.data).toBeNull();   // za wcześnie

  tick(500);             // kolejne 500ms - łącznie 1000
  expect(component.data).toBeTruthy(); // teraz gotowe
}));

Prześledź logikę czasu: po loadData dane są puste, bo timer jeszcze nie minął. Po pierwszym tick(500) wciąż puste - minęło dopiero pół sekundy. Dopiero drugi tick(500) domyka pełną sekundę i timer odpala. Ty decydujesz, kiedy "mija czas" - test jest natychmiastowy i w pełni przewidywalny. Gdy timerów jest wiele, flush() przewija je wszystkie naraz.

waitForAsync - poczekaj na prawdziwą asynchroniczność

fakeAsync udaje czas. Czasem jednak testujesz coś realnie asynchronicznego (np. zapytanie) i chcesz poczekać, aż się ustabilizuje. Do tego służy waitForAsync z whenStable.

import { waitForAsync } from '@angular/core/testing';

it('should fetch data asynchronously', waitForAsync(() => {
  const fixture = TestBed.createComponent(AsyncHttpComponent);
  const component = fixture.componentInstance;

  component.fetchData();

  fixture.whenStable().then(() => {
    fixture.detectChanges();
    expect(component.data).toBeTruthy();
  });
}));

Różnica jest w podejściu: fakeAsync mówi "przewiń zegar", a waitForAsync mówi "poczekaj, aż wszystkie zadania w tle się skończą" (whenStable). Prostą regułę zapamiętaj tak: przy znanym setTimeout sięgasz po fakeAsync, a przy nieokreślonych zadaniach w tle - po waitForAsync.

Testowanie sygnałów - najłatwiejsza rzecz w Angularze

Po zawiłościach czasu odetchniesz: sygnały testuje się trywialnie. Sygnał to pudełko z wartością - ustawiasz i odczytujesz, bez żadnej asynchroniczności.

import { signal, computed } from '@angular/core';

it('should update and derive signal values', () => {
  const count = signal(0);

  count.set(5);
  expect(count()).toBe(5);          // odczyt: count()

  const doubled = computed(() => count() * 2);
  expect(doubled()).toBe(10);       // wyliczony automatycznie

  count.set(3);
  expect(doubled()).toBe(6);        // sam się przeliczył
});

Cała sztuka to set (ustaw) i () (odczytaj). computed jest jeszcze przyjemniejszy: nie ustawiasz go ręcznie - sam przelicza się z sygnałów, od których zależy. Test to potwierdza: zmieniasz count, a doubled już ma nową wartość. Żadnego tick, żadnego czekania.

W komponencie dochodzi jeden krok - po zmianie sygnału wołasz detectChanges, by szablon się odświeżył:

it('should update template when signal changes', () => {
  component.count.set(42);
  fixture.detectChanges();  // odśwież widok
  expect(fixture.nativeElement.querySelector('.count').textContent).toContain('42');
});

it('should react to input signal changes', () => {
  fixture.componentRef.setInput('name', 'Musashi');  // dla input() używamy setInput
  fixture.detectChanges();
  expect(fixture.nativeElement.textContent).toContain('Musashi');
});

Zapamiętaj dwie różnice: zwykły sygnał ustawiasz przez .set(), ale sygnał wejściowy input() - przez fixture.componentRef.setInput(...), bo to rodzic go podaje, nie komponent sam sobie. Po każdej zmianie detectChanges przepisuje wartość do szablonu, i dopiero wtedy sprawdzasz treść.

Testowanie effectów

effect uruchamia kod przy każdej zmianie sygnału. Żeby go przetestować, potrzebujesz kontekstu wstrzykiwania i ręcznego uruchomienia efektów przez TestBed.flushEffects.

it('should trigger effect on signal change', () => {
  let effectCount = 0;

  TestBed.runInInjectionContext(() => {
    const count = signal(0);
    effect(() => { count(); effectCount++; });  // effect czyta count

    TestBed.flushEffects();
    expect(effectCount).toBe(1);   // pierwszy przebieg

    count.set(1);
    TestBed.flushEffects();
    expect(effectCount).toBe(2);   // effect zareagował na zmianę
  });
});

Effect nie odpala się natychmiast - Angular grupuje je i uruchamia w swoim rytmie, dlatego w teście wymuszasz to TestBed.flushEffects(). Licznik effectCount dowodzi rzeczy najważniejszej: effect wykonał się raz na starcie i drugi raz po zmianie sygnału. Gdyby liczba się nie zmieniła, znaczyłoby to, że effect nie śledzi count.

Pamiętaj z tej lekcji: w teście władasz czasem - fakeAsync + tick przy znanych timerach, waitForAsync przy zadaniach w tle. A sygnały to najłatwiejsza część: ustaw, detectChanges, sprawdź.

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co robi funkcja tick(1000) wewnątrz fakeAsync?

  2. 2. Kiedy używamy flush() zamiast tick() w fakeAsync?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij test asynchroniczny dla komponentu z debounce.

  • Układanie w pionie

    Uporządkuj kroki testu z debounce w fakeAsync (od góry do dołu):

  • Układanie w poziomie

    Ułóż wywołanie ustawiające wartość input signal w teście (jedna linia):

  • Klikanie w kolejności

    Ułóż linię ustawiającą nową wartość sygnału: count.set(42)

Przydatne artykuły