Kurs Angular · Moduł 11: Testowanie
Testowanie czasu i sygnałów
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. Co robi funkcja tick(1000) wewnątrz fakeAsync?
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)