Как тестировать асинхронный код в Angular?angular-97

Основные подходы к тестированию асинхронного кода

1. Использование async и fakeAsync утилит

Angular предоставляет специальные утилиты для работы с асинхронным кодом:

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

Разница между подходами:

  • async: Обертка для тестов с реальными асинхронными операциями
  • fakeAsync: Симуляция асинхронных операций с контролем времени

Тестирование с fakeAsync и tick

Пример тестирования таймера:

it('should update value after timeout', fakeAsync(() => {
  let value = 0;

  setTimeout(() => {
    value = 1;
  }, 1000);

  expect(value).toBe(0); // До таймера
  tick(1000); // Перемещаем время вперед
  expect(value).toBe(1); // После таймера
}));

Тестирование Observable с задержкой:

it('should handle delayed observable', fakeAsync(() => {
  const service = TestBed.inject(DataService);
  let result: any;

  service.getDelayedData().subscribe(data => {
    result = data;
  });

  tick(500); // Проходит 500ms
  expect(result).toBeUndefined();

  tick(500); // Еще 500ms (всего 1000ms)
  expect(result).toEqual({ id: 1 });
}));

Тестирование с async и whenStable

Пример для тестирования компонента с шаблоном:

it('should show data after async operation', async(() => {
  const fixture = TestBed.createComponent(AsyncComponent);
  const component = fixture.componentInstance;

  component.loadData(); // Запускает асинхронную операцию
  fixture.detectChanges();

  fixture.whenStable().then(() => {
    fixture.detectChanges(); // Обновляем шаблон после завершения асинхронных операций
    const element = fixture.nativeElement.querySelector('.data');
    expect(element.textContent).toContain('Loaded data');
  });
}));

Тестирование HTTP-запросов

Использование HttpClientTestingModule:

import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

describe('DataService', () => {
  let httpMock: HttpTestingController;
  let service: DataService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [DataService]
    });

    httpMock = TestBed.inject(HttpTestingController);
    service = TestBed.inject(DataService);
  });

  it('should return expected data', fakeAsync(() => {
    const testData = { id: 1, name: 'Test' };
    let response: any;

    service.getData().subscribe(data => {
      response = data;
    });

    const req = httpMock.expectOne('api/data');
    req.flush(testData);
    tick();

    expect(response).toEqual(testData);
  }));
});

Тестирование компонентов с async pipe

Пример для *ngIf с async:

it('should show content when data is loaded', fakeAsync(() => {
  const fixture = TestBed.createComponent(AsyncPipeComponent);
  const component = fixture.componentInstance;

  component.data$ = of({ id: 1 }).pipe(delay(100));
  fixture.detectChanges();

  expect(fixture.nativeElement.querySelector('.content')).toBeNull();

  tick(100);
  fixture.detectChanges();

  expect(fixture.nativeElement.querySelector('.content')).not.toBeNull();
}));

Тестирование Form Async Validators

Пример для асинхронного валидатора:

it('should validate username uniqueness', fakeAsync(() => {
  const control = new FormControl('taken_username');
  const validator = uniqueUsernameValidator(TestBed.inject(UserService));

  validator(control).subscribe(validationResult => {
    expect(validationResult).toEqual({ usernameTaken: true });
  });

  const req = httpMock.expectOne('/api/check-username');
  req.flush({ exists: true });
  tick();
}));

Обработка ошибок в асинхронном коде

Тестирование ошибок HTTP:

it('should handle 404 error', fakeAsync(() => {
  let errorResponse: any;

  service.getData().subscribe(
    () => fail('should have failed'),
    error => errorResponse = error
  );

  const req = httpMock.expectOne('api/data');
  req.flush('Not found', { status: 404, statusText: 'Not found' });
  tick();

  expect(errorResponse.status).toBe(404);
}));

Продвинутые техники

1. Тестирование нескольких асинхронных операций:

it('should handle multiple async operations', fakeAsync(() => {
  let value = '';

  of('first').pipe(
    delay(100),
    switchMap(() => of('second').pipe(delay(200)))
  ).subscribe(result => {
    value = result;
  });

  tick(100);
  expect(value).toBe('');

  tick(200);
  expect(value).toBe('second');
}));

2. Использование flush для микротасков:

it('should process microtasks', fakeAsync(() => {
  let value = 0;

  Promise.resolve().then(() => value = 1);
  expect(value).toBe(0);

  flush(); // Обрабатывает все микротаски
  expect(value).toBe(1);
}));

Лучшие практики

  1. Используйте fakeAsync для синхронного тестирования асинхронного кода
  2. Избегайте реальных таймеров - всегда мокируйте setTimeout/setInterval
  3. Комбинируйте подходы - fakeAsync для таймеров, async для сложных сценариев
  4. Не забывайте про tick() и flush() для управления временем
  5. Всегда проверяйте завершение теста - используйте httpMock.verify() для HTTP

Резюмируем

Angular предоставляет мощные инструменты (async, fakeAsync, tick, flush) для тестирования асинхронного кода. Для HTTP-запросов используйте HttpClientTestingModule, для таймеров - fakeAsync с tick, для сложных сценариев - async с whenStable. Правильное тестирование асинхронного кода критически важно для создания надежных Angular-приложений.